网络配置过程分析(linux网络协议栈笔记)
2016-12-01 21:19
543 查看
网络配置过程分析
在本篇,我们先介绍配置普通设备的IP地址的内部过程,接着再转到loopback接口的配置过程,这两个过程有相似之处,所以一起解说。然后再转入FIB系统,讲解路由系统,并用图例演示路由表的变化。最后介绍接口状态的变化,这对于驱动程序开发人员来说也是比较重要的。.1)配置是如何下达到内核的?
我们假设在安装我们的Linux系统时,没有配置IP地址,也没有挂上网线,完完全全是一台“裸机”,这样方便我们跟踪系统到底做了什么。安装完系统之后,我们可以使用ifconfig命令,查看设备信息。我想读者们应该都知道这么一个命令吧。在Windows上相应的操作是ipconfig。带上”-a”参数表示要查看详细的配置,包括loopbcack设备。
#ifconfig –a eth0 Link encap:Ethernet HWaddr 00:80:C8:EB:2A:39 BROADCAST MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 (0.0 B) TX bytes:0 ( 0.0 b) Interrupt:5 lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:16436 Metric:1 RX packets:6 errors:0 dropped:0 overruns:0 frame:0 TX packets:6 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 b) TX bytes:0 (0.0 b)
不同的机器和网卡会显示一些不同。在我的另外一台机器,其eth0的Interrupt等于3。
现在运行
#strace ifconfig eth0 192.168.18.2 netmask 255.255.255.0,配置好ip地址和网络掩码。为什么要运行strace?在大部分系统上是没有ifconfig的源代码的,那么为了查看ifconfig内部完成了什么操作时,可以用strace命令查看。它收集应用程序执行的系统调用,甚至参数都能记录下来。通过对这个命令的输出进行整理,我们可以把ifconfig内部调用的系统接口整理如下:
main(int argc, char **argv ) 1. { 2. struct sockaddr sa; 3. struct sockaddr_in sin; 4. char host[128]; 5. struct aftype *ap; 6. struct hwtype *hw; 7. struct ifreq ifr; 8. char **spp; 9. int fd; 10. 11. fd = socket 12. ifr.ifr_name = “eth0”; (PF_INET, SOCK_DGRAM, IPPROTO_IP); 13. ap = inet_aftype = 14. { 15. "inet", NULL, /*"DARPA Internet", */ AF_INET, sizeof(unsigned long), 16. INET_print, INET_sprint, INET_input, INET_reserror, 17. NULL /*INET_rprint */ , NULL /*INET_rinput */ , 18. INET_getnetmask, 19. -1, /* 这个值会被赋成fd,即刚才打开的socket */ 20. NULL 21. }; 22. host = “192.168.18.2”; 23. ap->input(0, host, &sa); /* 在此sa->sa_family=AF_INET,af->sa_data已经被设置成192.168.1.1 */ 24. memcpy((char *) &ifr.ifr_addr, (char *) &sa, sizeof(struct sockaddr)); 25. ioctl (fd, SIOCSIFADDR, &ifr); 26. ioctl (skfd, SIOCGIFFLAGS, &ifr); 27. ioctl 28. /* 开始第二次遍历命令行*/ (skfd, SIOCSIFFLAGS, &ifr); 29. host = “255.255.255.0”; 30. ioctl 31. (skfd, SIOCSIFNETMASK, ifr); 32. }
以上就是ifconfig这个命令实际内部“伪”代码,为了减轻复杂度,我们不必把整个代码列出来。从上面的代码中可以看出ifconfig实际调用了2个系统函数:socket和ioctl。4表示为socket打开的文件描述符,姑且认为这是应用程序到达系统内核的一个钥匙吧,后面会介绍一些相关知识。Socket的参数以后也会详细介绍。然后是ioctl这个系统调用。在Linux相关的项目中,ioctl是用户层与内核或设备驱动程序进行配置的一个有效手段。在这里我们要配置系统的地址,则必定要通过ioctl。在ifconfig的程序中依次调用6个ioctl,完成了对系统地址的配置。
下面我们就对出现的系统调用进行分析,首先是socket。
socket系统调用
从上图可以看到一个已安装的Linux操作系统究竟支持几种文件系统类型由文件系统的注册链表决定。在BSD socket层内使用msghdr{ }结构保存数据;在INET socket层以下都使用sk_buff{ }数据结构保存数据。
这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结构体现
struct socket { struct proto_ops *ops; struct file *file; struct sock *sk; wait_queue_head_t wait; short type; …… };
应用层中的操作对象是socket文件描述符,通过文件系统定义的通用接口,使用系统调用从用户空间切换到内核空间,控制socket文件描述符对应的就是对BSD socket的操作,从而进入到BSD socket层的操作。在BSD socket层中,操作对象是
socket{ }结构。每一个这样的结构对应的是一个网络连接。通过网络地址族的不同来区分不同的操作方法,判断是否应该进入到INET socket层,这一层的数据存放在
msghdr{ }结构中。
在INET socket层中,根据建立连接的类型,分成面向连接的和面向无连接两种类型,这是区分TCP和UDP协议的主要原则。这一层的操作对象主要是
sock{ }类型的数据,而数据存放在
sk_buff{ }结构中。
从INET socket层到IP层,主要是路由过程,发送时根据发送的目标地址确定需要使用的网络设备接口和下一需要传送的机器地址。
在内核中与socket对应的系统调用是
sys_soceket,所谓的创建套接口,就是在sockfs这个文件系统中创建一个节点,从Linux/Unix的角度来看,该节点是一个文件,不过这个文件具有非普通文件的属性,于是起了一个独特的名字——socket。由于sockfs文件系统是系统初始化时就保存在全局指针
sock_mnt中的,所以申请一个inode的过程便以
sock_mnt为参数。从进程角度看,一个套接口就是一个特殊的已打开文件。现在将socket结构的宿主inode与文件系统挂上钩,就是分配文件号以及file结构在目录数中分配一个dentry结构。指向inode使
file->f_dentry指向inode建立目录项和索引节点(即套接口的节点名) 现在我们来看看每个进程是如何对待其打开的文件及socket的:
从上图中可以看到,标准输入接口(std in)的文件描述符占据了进程的用户空间的文件描述符数组的第一个单元,这是系统内定的。通过内核的组织,其在内核中的描述符数组的第一个单元存放了指向其inode的地址,以此类推,标准输出接口(std out)占据第二个单元,错误输出接口(std err)占据第三个单元(上图没有画出)。而用户自己打开的文件或socket,则依次排列到系统已经内定的错误输出接口的fd单元后面,比如,如果某进程第一次打开了一个常规文件,它的fd必定是3,当再打开别的文件时,fd就是4,这里3和4就是用户空间中的fd数组下标。同样的道理,socket也可以看作是一个文件。每次调用socket返回的fd值也是指用户空间的fd数组下标。
现在我们来看看socket函数本身,经过glibc库对其封装,它将通过
int 0x80产生一个软件中断(注意不是软中断),由内核导向执行
sys_socket,基本上参数会原封不动地传入内核,它们分别是(1)
int family, (2)
int type, (3)
int protocol。其调用树如下图:
由于不关心security方面的代码,我们就不研究
security_socket_create函数了。直接对
sock_alloc进行分解,
struct socket{}结构就是它创建的,它的调用树如下:
我们的
mnt_sb->s_op->alloc_inode就是静态全局变量
super_operations sockfs_ops定义的
sock_alloc_inode函数。它仅仅是用
kmem_cache_alloc创建一个
socket_alloc{}类型的inode,然后返回给
alloc_inode进行初始化。所以纵观
sock_alloc( )的作用就是分配及初始化属于网络类型(应该是
socket_alloc类型)的inode。inode结构中的大部分字段只是对真正的文件系统重要,但是,只有一部分由socket使用。
socket_alloc类型的inode结构如下:
此图中的socket socket部分就是“FD的意义”一图中那个“特定文件的数据”部分。那么
socket{ }结构就表示这是一个跟网络有关的文件描述符,是INET层与应用层打开的文件描述一一对应的实体,每一次调用socket函数都会在INET中保存这么一个实体。
在常规文件系统中常用的open系统调用不能用来创建一个socket,sockets只能用socket API创建。一旦socket被创建,IO操作就和普通文件或设备一样了。
sock_alloc实际上创建了socket结构并从socket
inode slab cache中创建inode结构。
SOCKET_I和
SOCK_INODE可以相互转换inode和socket。内核通过fd找到内存中对应的inode,然后再通过VFS系统查找到对应的内核模块,这样用户空间和内核协议栈就搭上了关系。一旦inode被创建,socket层就可以映射IO系统调用了。一个
file_operations结构,
socket_file_ops被创建并初始化,其中所有的IO系统调用都有对应的socket对应操作
(注:open调用返回一个ENXIO错误)
1. /* 2. Socket类型的文件系统和其他文件系统一样有一个特别的操作集合, 但是有相当一部分操作是经过直接调用系统接口而不是通过这个操作集合来完成。 3. */ 4. 5. static struct file_operations socket_file_ops = { 6. .owner = THIS_MODULE, 7. .llseek = no_llseek, 8. ...... 9. .poll = sock_poll, 10. .unlocked_ioctl = sock_ioctl, 11. .open = sock_no_open, /* 这个open操作明显是socket系统不支持的*/ 12. .release = sock_close, 13. ...... 14. .readv = sock_readv, 15. .writev = sock_writev, 16. ....... 17. };
到此为止,我们可以作出结论,VFS为了使socket系统工作——或者说socket为了适应VFS系统框架,socket提供了2个数据结构:
super_operations——
sockfs_ops和
file_operations——
socket_file_ops。前者是必须的,它创建了VFS必需的inode,使VFS可以对其进行文件级别的管理;而后者是可选的,用户层可以使用标准文件系统的操作比如
write( )和
read( )对socket对象进行操作,也可以采用系统提供的
send( )和
recv( )接口对socket对象进行处理,但二者都归一到网络内部实现代码中。
在ifconfig的代码中socket系统调用的第一个参数即family是
PF_INET,意味着
socket_create中的
net_families[family]->create(sock, protocol)指的是
net_families[PF_INET]里的
inet_create函数,它在“网络系统初始化”中提及。而第二个参数type被赋给
sock->type成员,而后根据这个type获取
inetsw[]数组中相对应的成员。
我们再来看
inet_create代码:
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern) { struct sock *sk; struct inet_protosw *answer; struct inet_sock *inet; struct proto *answer_prot; unsigned char answer_flags; char answer_no_check; int try_loading_module = 0; int err; sock->state = SS_UNCONNECTED; /* Look for the requested type/protocol pair. */ lookup_protocol: err = -ESOCKTNOSUPPORT; rcu_read_lock(); /* 从inetsw中根据类型、协议查找相应的socket interface也就是 inet_protosw */ list_for_each_entry_rcu(answer, &inetsw[sock->type], list) { err = 0; /* Check the non-wild match. */ if (protocol == answer->protocol) { if (protocol != IPPROTO_IP) break; } else { /* Check for the two wild cases. */ if (IPPROTO_IP == protocol) { protocol = answer->protocol; break; } if (IPPROTO_IP == answer->protocol) break; } err = -EPROTONOSUPPORT; } /*如果没有找到,尝试加载模块*/ if (unlikely(err)) { if (try_loading_module < 2) { rcu_read_unlock(); /* * Be more specific, e.g. net-pf-2-proto-132-type-1 * (net-pf-PF_INET-proto-IPPROTO_SCTP-type-SOCK_STREAM) */ if (++try_loading_module == 1) request_module("net-pf-%d-proto-%d-type-%d", PF_INET, protocol, sock->type); /* * Fall back to generic, e.g. net-pf-2-proto-132 * (net-pf-PF_INET-proto-IPPROTO_SCTP) */ else request_module("net-pf-%d-proto-%d", PF_INET, protocol); goto lookup_protocol; } else goto out_rcu_unlock; } err = -EPERM; if (sock->type == SOCK_RAW && !kern && !ns_capable(net->user_ns, CAP_NET_RAW)) goto out_rcu_unlock; sock->ops = answer->ops; answer_prot = answer->prot; answer_no_check = answer->no_check; answer_flags = answer->flags; rcu_read_unlock(); WARN_ON(answer_prot->slab == NULL); /* sk_alloc表面上是生成一个sock的结构体,但是实际上对于tcp来说是一个tcp_sock的大小的结构体,这样就可以使用inet_sk(sk);进行强制的类型转换,具体是怎么分配的是tcp_sock大小的,在后续进行分析*/ err = -ENOBUFS; sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot); if (sk == NULL) goto out; err = 0; sk->sk_no_check = answer_no_check; if (INET_PROTOSW_REUSE & answer_flags) sk->sk_reuse = SK_CAN_REUSE; inet = inet_sk(sk); inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0; inet->nodefrag = 0; if (SOCK_RAW == sock->type) { inet->inet_num = protocol; if (IPPROTO_RAW == protocol) inet->hdrincl = 1; } if (net->ipv4.sysctl_ip_no_pmtu_disc) inet->pmtudisc = IP_PMTUDISC_DONT; else inet->pmtudisc = IP_PMTUDISC_WANT; inet->inet_id = 0; /*对sk结构体中的变量进行初始化操作,*/ sock_init_data(sock, sk);------------------(1) sk->sk_destruct = inet_sock_destruct; sk->sk_protocol = protocol; sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv; inet->uc_ttl = -1; inet->mc_loop = 1; inet->mc_ttl = 1; inet->mc_all = 1; inet->mc_index = 0; inet->mc_list = NULL; inet->rcv_tos = 0; sk_refcnt_debug_inc(sk); if (inet->inet_num) { /* It assumes that any protocol which allows * the user to assign a number at socket * creation time automatically * shares. */ inet->inet_sport = htons(inet->inet_num); /* Add to protocol hash chains. */ sk->sk_prot->hash(sk); } if (sk->sk_prot->init) { err = sk->sk_prot->init(sk);//如果是tcp的话,这里就是tcp_v4_init_sock--------(2) if (err) sk_common_release(sk); } out: return err; out_rcu_unlock: rcu_read_unlock(); goto out; }
在这段代码中又发现一个极易引起混乱的结构体名字——
sock{ },它和
socket{ }结构的名字只差了两个字母,它和sock确实有关系。为何?
socket{}结构表示INET中的实体,而
sock{ }结构就是网络层实体,sock用来保存打开的连接的状态信息。它一般定义的变量叫sk。
socket系统调用的第三个参数是protocol,在这里它终于派上用场——赋给
sk->sk_protocol,不过,在ifconfig这个应用中,它没有起到作用。
backlog_rcv在此根据协议分别是
tcp_v4_do_rcv,
udp_queue_rcv_skb或者是
raw_rcv_skb,在ifconfig的这个应用中,则是第二个。不过,由于目前只是配置,我就不在这里介绍它了,而是放在后面去分析这个函数。
根据对
sys_socket的分析,我们可以推断出
file{ }、
socket{ }、
sock{ }之间的关系如下:
会通过
sock_map_fd指向一个file指针。这个文件指针用来维护与该socket相关联的伪文件的状态。也就是说,不管你的应用程序是基于TCP的还是基于IP甚至是跟网络传输没有关系(例如ifconfig),内核会在INET和Network层分别创建一个实体来对应你打开的那个文件描述符,而后你对该文件描述符的操作都是通过这两个数据结构的实体去完成。因为系统为每次socket系统调用都创建一对
<file, socket,sock>三元组,所以每个应用程序打开的每一个文件描述符都不会出现互斥操作,也就避免了锁的开销。每次应用程序要访问内核模块时,都通过fd去操作,内核会在fd对应的数组单元内查找到相应的socket,然后才能将后继的操作进行下去,比如我们马上要分析的ioctl。
在
inet_create函数中创建
sock{}的时候调用
sk_alloc接口,它根据协议的类型来创建真正的“sock”结构,因为协议不同,实际创建的对象不同,虽然函数返回值都是
sock{}指针,但是实际的大小并不相同,比如,对于raw应用,我们应该创建的是
raw_sock,而对于tcp来说,应该创建的是
tcp_sock,由于在创建sock{}时就已经根据协议类型为其准备了足够大的空间,所以当进入到协议相关的代码部分,使用指针类型强制转型,将其转换为相应的数据结构。
INET socket层支持包括TCP/IP协议在内的internet地址族。如前所述,这些协议是分层的,一个协议使用另一个协议的服务。Linux的TCP/IP代码和数据结构反映了这一分层模型。BSD socket层从已注册的INET
proto_ops数据结构中调用INET层 socket支持例程来为它执行工作。例如,一个地址族为INET的BSD socket建立请求,将用到下层的INET socket的建立函数。为了不把BSD socket 与TCP/IP的特定信息搞混,INET socket层使用它自己的数据结构sock,它与BSD socket结构相连。这一关系意味着后来的INET socket调用能够很容易地重新找到sock结构。sock结构的协议操作指针也在初始化时建立,它依赖与被请求的协议。如果请求的是TCP,那么sock结构的协议操作指针将指向TCP连接所必需的TCP协议操作集。
上图中演示了各种协议下不同
sock{}的本质,先列出一些相应的强转类型的宏或函数,打好基础:
sk宏的输入输出比较
宏/函数 | 输入结构类型 | 输出结构类型 |
---|---|---|
inet_sk | sock{} | inet_sock{} |
udp_sk | sock{} | udp_sock{} |
tcp_sk | sock{} | tcp_sock{} |
raw_sk | sock{} | raw_sock{} |
ioctl代码的实现
ioctl是标准库里的函数,实际上也就是说大部分操作系统都会支持这个系统调用。它是内核模块和应用程序交互配置的一种手段,而且,它是同步完成的,即它的代码必定处于某个进程的上下文中。那还有什么交互手段不是这样的呢?答案是netlink接口。它是异步完成配置的工作的,比如读写路由表。先让我们目光集中在ioctl上。配置设备IP地址的内核处理过程
ioctl函数在内核中对应的函数是
sys_ioctl,这个接口的操作属于VFS的,也就是说系统不区分用户要对哪种文件系统进行ioctl,只有根据用户之前打开的文件类型来推断正确的调用路径。比如socket类型的文件系统,它的ioctl必定是由
socket_file_ops结构定义的。内核中具体的执行路径如下:
如果你要编写关于网络部分的ioctl应用程序代码,那么专门有一个数据结构来作为ioctl的参数给你,对于标准的内核,你必须使用该数据结构:
struct ifreq { #define IFHWADDRLEN 6 union { char ifrn_name[IFNAMSIZ]; /* if name, e.g. "en0" */ } ifr_ifrn; union { struct sockaddr ifru_addr; struct sockaddr ifru_dstaddr; struct sockaddr ifru_broadaddr; struct sockaddr ifru_netmask; struct sockaddr ifru_hwaddr; short ifru_flags; int ifru_ivalue; int ifru_mtu; struct ifmap ifru_map; char ifru_slave[IFNAMSIZ]; /* Just fits the size */ char ifru_newname[IFNAMSIZ]; void __user * ifru_data; struct if_settings ifru_settings; } ifr_ifru; };
这个结构在内核中也有同样的定义,所以在
devinet_ioctl函数就用
copy_from_user(&ifr, arg, sizeof(struct ifreq))这样一条语句搞定,arg是void *变量,实际在内部实现中已经变成了
unsigned long变量,也就是指针变量。
Ifconfig调用的ioctl码依次是
SIOCSIFADDR,
SIOCSIFFLAGS,
SIOCSIFNETMASK。
下面是
devinet_ioctl函数的部分代码:
int devinet_ioctl(unsigned int cmd, void *arg) { struct ifreq ifr; struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_addr; struct in_device *in_dev; struct in_ifaddr **ifap = NULL; struct in_ifaddr *ifa = NULL; struct net_device *dev; char *colon; int ret = 0; /* * Fetch the caller's info block into kernel space */ if (copy_from_user(&ifr, arg, sizeof(struct ifreq))) return -EFAULT; ifr.ifr_name[IFNAMSIZ-1] = 0; colon = strchr(ifr.ifr_name, ':'); if (colon) *colon = 0; #ifdef CONFIG_KMOD dev_load(ifr.ifr_name); #endif switch(cmd) { //根據cmd對參數進行檢查 case SIOCGIFADDR: /* Get interface address */ case SIOCGIFBRDADDR: /* Get the broadcast address */ case SIOCGIFDSTADDR: /* Get the destination address */ case SIOCGIFNETMASK: /* Get the netmask for the interface */ /* Note that this ioctls will not sleep, so that we do not impose a lock. One day we will be forced to put shlock here (I mean SMP) */ memset(sin, 0, sizeof(*sin)); sin->sin_family = AF_INET; break; case SIOCSIFFLAGS: if (!capable(CAP_NET_ADMIN)) return -EACCES; break; case SIOCSIFADDR: /* Set interface address (and family) */ case SIOCSIFBRDADDR: /* Set the broadcast address */ case SIOCSIFDSTADDR: /* Set the destination address */ case SIOCSIFNETMASK: /* Set the netmask for the interface */ if (!capable(CAP_NET_ADMIN)) return -EACCES; if (sin->sin_family != AF_INET) return -EINVAL; break; default: return -EINVAL; } //鎖 dev_probe_lock(); rtnl_lock(); //獲取設備名對應的設備的數據結構(描述一個設備) if ((dev = __dev_get_by_name(ifr.ifr_name)) == NULL) { ret = -ENODEV; goto done; } if (colon) *colon = ':'; if ((in_dev=__in_dev_get(dev)) != NULL) { //查找對應的設備名 for (ifap=&in_dev->ifa_list; (ifa=*ifap) != NULL; ifap=&ifa->ifa_next) if (strcmp(ifr.ifr_name, ifa->ifa_label) == 0) break; } if (ifa == NULL && cmd != SIOCSIFADDR && cmd != SIOCSIFFLAGS) { ret = -EADDRNOTAVAIL; goto done; } switch(cmd) { //在對應的設備上進行操作 case SIOCGIFADDR: /* Get interface address */ //獲取IP sin->sin_addr.s_addr = ifa->ifa_local; goto rarok; case SIOCGIFBRDADDR: /* Get the broadcast address */ sin->sin_addr.s_addr = ifa->ifa_broadcast; goto rarok; case SIOCGIFDSTADDR: /* Get the destination address */ sin->sin_addr.s_addr = ifa->ifa_address; goto rarok; case SIOCGIFNETMASK: /* Get the netmask for the interface */ sin->sin_addr.s_addr = ifa->ifa_mask; goto rarok; case SIOCSIFFLAGS: if (colon) { if (ifa == NULL) { ret = -EADDRNOTAVAIL; break; } if (!(ifr.ifr_flags&IFF_UP)) inet_del_ifa(in_dev, ifap, 1); break; } ret = dev_change_flags(dev, ifr.ifr_flags); break; case SIOCSIFADDR: /* Set interface address (and family) */ if (inet_abc_len(sin->sin_addr.s_addr) < 0) { ret = -EINVAL; break; } if (!ifa) { if ((ifa = inet_alloc_ifa()) == NULL) { ret = -ENOBUFS; break; } if (colon) memcpy(ifa->ifa_label, ifr.ifr_name, IFNAMSIZ); else memcpy(ifa->ifa_label, dev->name, IFNAMSIZ); } else { ret = 0; if (ifa->ifa_local == sin->sin_addr.s_addr) break; inet_del_ifa(in_dev, ifap, 0); ifa->ifa_broadcast = 0; ifa->ifa_anycast = 0; } ifa->ifa_address = ifa->ifa_local = sin->sin_addr.s_addr; //設置IP if (!(dev->flags&IFF_POINTOPOINT)) { ifa->ifa_prefixlen = inet_abc_len(ifa->ifa_address); ifa->ifa_mask = inet_make_mask(ifa->ifa_prefixlen); if ((dev->flags&IFF_BROADCAST) && ifa->ifa_prefixlen < 31) ifa->ifa_broadcast = ifa->ifa_address|~ifa->ifa_mask; } else { ifa->ifa_prefixlen = 32; ifa->ifa_mask = inet_make_mask(32); } ret = inet_set_ifa(dev, ifa); break; case SIOCSIFBRDADDR: /* Set the broadcast address */ if (ifa->ifa_broadcast != sin->sin_addr.s_addr) { inet_del_ifa(in_dev, ifap, 0); ifa->ifa_broadcast = sin->sin_addr.s_addr; inet_insert_ifa(ifa); } break; case SIOCSIFDSTADDR: /* Set the destination address */ if (ifa->ifa_address != sin->sin_addr.s_addr) { if (inet_abc_len(sin->sin_addr.s_addr) < 0) { ret = -EINVAL; break; } inet_del_ifa(in_dev, ifap, 0); ifa->ifa_address = sin->sin_addr.s_addr; inet_insert_ifa(ifa); } break; case SIOCSIFNETMASK: /* Set the netmask for the interface */ /* * The mask we set must be legal. */ if (bad_mask(sin->sin_addr.s_addr, 0)) { ret = -EINVAL; break; } if (ifa->ifa_mask != sin->sin_addr.s_addr) { inet_del_ifa(in_dev, ifap, 0); ifa->ifa_mask = sin->sin_addr.s_addr; ifa->ifa_prefixlen = inet_mask_len(ifa->ifa_mask); inet_insert_ifa(ifa); } break; } done: rtnl_unlock(); dev_probe_unlock(); return ret; rarok: rtnl_unlock(); dev_probe_unlock(); if (copy_to_user(arg, &ifr, sizeof(struct ifreq))) return -EFAULT; return 0; }
在上面的switch语句
SIOCSIFADDR分支中我们分配了
in_ifaddr{}结构,然后把它传入下面这个函数
inet_set_ifa,它的基本内容是申请
in_dev{}结构,然后把它挂到ifa结构中,并把这个ifa存入其源代码如下:
static int inet_set_ifa(struct net_device *dev, struct in_ifaddr *ifa) { struct in_device *in_dev = __in_dev_get(dev); if (!in_dev) { in_dev = inetdev_init(dev); ...... } if (ifa->ifa_dev != in_dev) { ifa->ifa_dev = in_dev; } /**如果是loopback地址,那么该地址的scope是本机范围,否则还是初始值0,即UNIVERSE。**/ if (LOOPBACK(ifa->ifa_local)) ifa->ifa_scope = RT_SCOPE_HOST; return inet_insert_ifa(ifa); }
如果在该设备上没有找到相应的IP地址配置,就创建一个
in_device结构。
static struct in_device *inetdev_init(struct net_device *dev) { struct in_device *in_dev; ASSERT_RTNL(); in_dev = kzalloc(sizeof(*in_dev), GFP_KERNEL); if (!in_dev) goto out; memcpy(&in_dev->cnf, dev_net(dev)->ipv4.devconf_dflt, sizeof(in_dev->cnf));//这里对in_dev->cnt进行初始化操作, in_dev->cnf.sysctl = NULL; in_dev->dev = dev; if ((in_dev->arp_parms = neigh_parms_alloc(dev, &arp_tbl)) == NULL) goto out_kfree; if (IPV4_DEVCONF(in_dev->cnf, FORWARDING)) dev_disable_lro(dev); /* Reference in_dev->dev */ dev_hold(dev); /* Account for reference dev->ip_ptr (below) */ in_dev_hold(in_dev); devinet_sysctl_register(in_dev); ip_mc_init_dev(in_dev); if (dev->flags & IFF_UP) ip_mc_up(in_dev); /* we can receive as soon as ip_ptr is set -- do this last */ rcu_assign_pointer(dev->ip_ptr, in_dev);//使用RCU保护锁机制对dev->ip_ptr进行赋值 out: return in_dev; out_kfree: kfree(in_dev); in_dev = NULL; goto out; }
经过这么一番配置,
in_ifaddr{},
net_device{}和
in_device{}的关系如下图,而且要记住,
in_ifaddr{}这个结构里的成员不会被其它模块改变了:
经过上面的分析,代码的大致流程如下:
首先调用
rtmsg_ifa发送消息给应用层,以至于netlink的侦听函数会知道这个新地址,然后调用通知链,这会触发对地址分配IP事件感兴趣的等候者行动。对ioctl分析也快到头了,但好像还没看到跟路由有什么关系,难道线索断了?但是我们忘了,
blocking_notifier_call_chain扫描的是
inetaddr_chain,顾名思义,这是一条链呀,对了,还记得在FIB初始化的时候,它曾经也挂到了这个链上吗?
netlink和rtnetlink接口
虽然我们还没有说到路由系统,但是为了解释刚才看见的netlink_broadcast函数,没办法。通常,在Linux里当一个应用程序想要增加或删除一个路由时,它会用nlmsghdr+rtmsg结构并传给rtnetlink socket。
rtentry结构是UNIX系统提供给用户接口获取路由的传统途径,实际上Linux内部并不使用这个结构,它和BSD操作系统用作路由ioctl的rtentry相似,只是用来方便Linux和其他UNIX变种的移植。一个指向rtentry的指针作为参数传到ioctl系统调用,然后通过
fib_convert_rtentry(int cmd, struct nlmsghdr *nl, struct rtmsg *rtm, struct kern_rta *rta, struct rtentry *r)函数把这个结构的成员分拆到
nlmsghdr{}、
rtmmsg{}、
kern_rta{}结构中。
访问FIB的主要方法是通过netlink socket,它为路由提供了扩展——rtnetlink。netlink是一个内部的通信协议。它主要用来在应用层和Linux内核里大量协议之间传递信息。
Netlink实现了自己的地址族——
AF_NETLINK,它支持大多数的socket API函数。netlink最常用的场合是应用程序预内核内部的路由表交换路由信息。
其使用方法是先打开一个socket:
fd = socket(PF_NETLINK, socket_type, netlink_family);
socket_type为
SOCK_RAW或者
SOCK_DGRAM都可以,因为netlink本身是基于数据报的。比较常用
netlink_family有下面几种:
-
NETLINK_ROUTE用来修改和读取路由表的,这是我们后面要讨论的rtnetlink问题.
-
NETLINK_ARPD在用户空间中管理ARP表.
-
NETLINK_USERSOCK给应用程序(不一定都是协议)发送的消息
打开了socket,那么可以用sendmsg和recvmsg给内核或同台主机应用程序(而不是发给远端主机)发消息。这些调用传递一个指向nlmsghdr结构的指针(见上图)。rtnetlink 是基本netlink协议的消息扩展。也就是当创建
PF_NETLINK的socket时,把
netlink_family设置为
NETLINK_ROUTE。内核创建了一个轻量级的socket,专门用于接收来自用户netlink接口发送过来的消息,也接收内核部分的消息。
内核模块使用
netlink_broadcast给rtnetlink发送消息,由
rtnetlink_rcv函数接收并处理。下面继续解说
inet_insert_ifa函数,它里面利用了一个C语言技巧,首先它创建一个skb,然后进入一个判断语句,首先判断
inet_fill_ifaddr函数执行的结果,在大部分情况下,必定会进入到最后一个else语句,执行
netlink_broadcast,这种技巧在内核代码中比较常见,而且通常能造成读者迷惑,认为最后一句else的情况不会到达。这种技巧的目的在于遍历各种情况,而且不影响代码的“从上到下”顺序。
static void rtmsg_ifa(int event, struct in_ifaddr* ifa) { int size = NLMSG_SPACE(sizeof(struct ifaddrmsg) + 128); struct sk_buff *skb = alloc_skb(size, GFP_KERNEL); if (inet_fill_ifaddr(skb, ifa, current->pid, 0, event, 0) < 0) { //错误处理 } else { //下面这个函数会通知阻塞在某个netlink下的回调函数,然后让它们进行处理。不过要注意,rtnl是一个sock{}结构的全局变量 netlink_broadcast(rtnl, skb, 0, RTNLGRP_IPV4_IFADDR, GFP_KERNEL); } }
当
rtnetlink_rcv接收到这样一个消息,它会根据参数调用对应的函数表。在这里它会调用
inet_rtm_newaddr函数。
inet_rtm_newroute增加一个新的路由到FIB。
inet_rtm_delroute从FIB中删除一条路由。这两个函数从紧跟nlmsghdr后面的内存中抽取rtmsg结构,rtmsg的结构如下:
struct rtmsg { unsigned char rtm_family; /*路由地址族*/ unsigned char rtm_dst_len; /*length of destination*/ unsigned char rtm_src_len; /*length of source*/ unsigned char rtm_tos; /*TOS filter*/ unsigned char rtm_table; /*Routing table id*/ unsigned char rtm_protocol; /*路由协议*/ unsigned char rtm_scope; unsigned char rtm_type; unsigned int rtm_flags; }
Rtmsg结构的协议字段的值如果大于
RTPROT_STATIC,那么内核不会改变它,只是在用户和内核空间传递。它们故意留着给假想的几个路由守护进程使用。
相关文章推荐
- 网络配置过程分析二(linux网络协议栈笔记)
- linux网络协议栈分析笔记10-arp邻居子系统3
- linux网络协议栈分析笔记11-路由1-路由缓存
- linux网络协议栈分析笔记3-网桥2
- linux网络协议栈分析笔记2-网桥1
- [置顶] Linux协议栈代码阅读笔记(二)网络接口的配置
- FIB系统分析二(linux网络协议栈笔记)
- Linux协议栈代码阅读笔记(二)网络接口的配置
- Linux网管笔记(29)Ubuntu Linux服务器网络配置过程
- linux网络协议栈分析笔记1-接入部分
- linux网络协议栈分析笔记9-arp邻居子系统2
- linux网络协议栈分析笔记4-网桥3
- linux网络协议栈分析笔记5-IP层的处理1
- linux网络协议栈分析笔记12-路由2-FIB1
- RedHat Linux网络配置过程笔记
- Linux网络协议栈--ip_append_data函数分析
- linux网络协议栈源码分析
- linux下网络配置小节[from 老男孩的linux运维笔记]
- 菜鸟学习linux笔记与练习-----一些基本命令以及初级网络配置
- linux网络协议栈分析——net_families、inetsw、inetsw_array、inet_protos