UDP 通讯及内部分析(合集)

https://blog.csdn.net/arau_sh/article/details/2981347

http://hi.baidu.com/linux_kernel/blog/item/c7166d8128d418d8bd3e1ee3.html (失效)


在未连接的Socket上发送UDP数据报


UDP是一个简单的面向数据报的传输层协议,我们先站在UDP客户端的角度来看看如何发送一个UDP数据报,以及协议栈为发送一个UDP数据报做了哪些事情。

UDP数据报可以在未连接的socket上发送(使用sendto系统调用,指定目的地址),也可以在已连接的socket上发送(使用send系统调用,不用指定目的地址),下面我们分两种情况讨论。

下面是一个在未连接的socket上发送UDP数据的用户态程序示例(注:该程序的格式和风格相当不好,只是为临时测试使用。),该程序目前还只管发送,不处理接收,关于接收,我们后面再作分析:

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>

#include <arpa/inet.h>
#include <unistd.h>

int main()
{
    int i;
    struct sockaddr_in dest;
    dest.sin_family = MY_PF_INET;
    dest.sin_port = htons(16000);
    dest.sin_addr.s_addr = 0x013010AC;  //目的地址是172.16.48.1(网络字节序)

    //创建UDP数据报服务的socket。
    int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
    if( fd < 0 ){
        perror("socket: ");
        return -1;
    }
    int bwrite = sendto( fd, "abcdefg", 7, 0, (struct sockaddr *)&dest, sizeof(dest) );
    if( bwrite == -1 ){
        perror("send: ");
        close(fd);
        return -1;
    }
    printf("sendto: %d/n", bwrite);
    close( fd );
    return 0;
}

创建socket的操作跟RAW协议的差不多,只有极少区别,内核中表示套接字的结构上的操作集,协议名略有不同而已。我们重点看sendto操作所引发的内核代码执行。sendto所到达的my_inet模块的第一站是myinet_sendmsg,一般来讲,该函数只要调用udp协议自己的udp_sendmsg即可,但在之前,它还有一样事情要完成,就是为这个socket执行绑定,这个绑定可能跟服务器端的bind系统调用有些区别。试想,如果我们用这个udp socket发送出去了一个数据报,但没有记录下这个udp socket,那等对端的回应数据报来的时候,我们就不知道哪个socket要接收这个数据报了。绑定就是记录这个udp socket。

myudp_hash是一个具有128项的哈希数组,每一项都是一个udp socket的链表,每个udp socket以自己的源地址端口号为哈希主键插入这个数组。源地址端口可以是用户自己指定的,也可以是由内核自动分配的。

内核自动分配的源端口号有一个范围,这个范围段似乎是由系统的内存大小决定的(具体有待进一步分析),如果内存大(似乎是有高端内存可用),范围段是32768-61000,否则就是1024-4999。udp_port_rover是一个全局变量,初始值为范围段的下限,每次新分配端口,记录下新分配的端口号,下一次再分配时,在前一次的基础上加1,然后查询对应的myudp_hash中的项,如果该项的链表不为空,则找下一项,直至遍历整个数组,如果为空,则分配成功。所以,当连续分配128个端口后(数组中的128项中,链表全不为空),这个查询必然失败,最后遍历数组完成时,得到的端口号必然是前一次分配的端口号加127,然后,端口号每次加128,再查询对应的数组项,看该端口号有没有被使用掉。

这个描述可能有点模糊,简单总结一下就是:每次分配一个端口号,先在前一次分配值的基础上以1为步进值递增,如果对应的哈希数组中的链表为空,则肯定没有被使用过,直接使用。如果遍历完整个哈希表都没有空的链表,则要查询链表中的每一项,以得到未使用的端口。

用户自己指定一个端口,则我们到对应的哈希数组中的链表查询,如果已被使用,并且不能重用,则分配端口号失败。对用户自己指定的端口,没有范围段的限制。这个一般用于服务端,而自动分配端口用于客户端。

绑定完成后,myinet_sendmsg会调用myudp_sendmsg,它与myraw_sendmsg所执行的操作相差并不多。先查询输出路由,然后添加协议首部,最后发送数据包。与raw相比,udp要在IP首部前添加一个UDP首部。以下是UDP首部的定义:

struct udphdr {
    __u16   source;     //发送端端口号。
    __u16   dest;       //目的端端口号。
    __u16   len;        //UDP长度。
    __u16   check;      //UDP检验和。
};

UDP是一个传输层协议,与下层的网络层协议相比,它不仅需要知道数据传输的两端的主机,还需要知道是主机上的哪个进程在进行数据传输,端口号其实就是用于标识发送进程和接收进程的。UDP长度是UDP头加上UDP数据的长度(不包括IP首部)。UDP检验和覆盖UDP首部和UDP数据。

由于UDP数据报在未连接的socket上进行发送,所以每次进入myraw_sendmsg,都要进行输出路由的查询,以确定源地址和目的地址。但我们知道,路由是有缓存的,所以,并没有太多的额外开销。认为在未连接的socket上发送UDP数据报开销要大的观点并不完全正确。


在一个已连接的socket上发送UDP数据报


很多介绍网络编程的书籍中会这样介绍connect系统调用:将本机的一个指定的套接字连接到一个指定地址的服务器套接字上去。下面是connect系统调用的定义:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
参数sockfd是本地机器上的一个套接字描述符,在内核的系统调用函数中该描述符会被转换成与之绑定的一个struct socket结构,这是真正的一个socket,代表了网络通讯中连接的一端。serv_addr和addrlen则是要连接的服务器的地址和地址长度。

于是乎,有了这样的理解:connect将在本机和指定服务器间建立一个连接。但实际上,connect操作并不引发网络设备传送任何的数据到对端。它所做的操作只是通过路由规则和路由表等一些信息,在struct socket结构中填入一些有关对端服务器的信息。这样,以后向对端发送数据报时,就不需要每次进行路由查询等操作以确定对端地址信息和本地发送接口,应用程序也就不需要每次传入对端地址信息(可以使用send而不使用sendto)。基于这样的理解,我们就不难弄明白,为什么不只是tcp socket可以connect,udp, raw socket也可以通过connect进行连接。它们的本质其实没有多大差别:把通过路由查询得到的对端主机的地址信息缓存到套接字结构struct socket中。

udp和raw的connect操作其实是完全一致的,都使用了myip4_datagram_connect函数。
为方便起见,我们再以一个实际的例子来描述该函数所做的事情,我们在主机172.16.48.2上向主机172.16.48.1的端口16000发送一个udp数据报,172.16.48.2上的udp端口由系统自动选择(为32768)。下面是一个简单的应用程序示例:

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>

#include <arpa/inet.h>
#include <unistd.h>

int main()
{
    int i;
    //代表服务器地址的结构。
    struct sockaddr_in dest;
    dest.sin_family = MY_PF_INET;
    dest.sin_port = htons(16000);
    dest.sin_addr.s_addr = 0x013010AC;//172.16.48.1的网络字节序。

    int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
    if( fd < 0 ){
        perror("socket: ");
        return -1;
    }
    //连接操作。
    if( connect( fd, (struct sockaddr*)&dest, sizeof(dest) ) < 0 )
        perror("connect: ");
    //不必通过sendto每次传入对端地址信息了。
    int bwrite = send( fd, "abcdefg", 7, 0 );
    if( bwrite == -1 ){
        perror("send: ");
        return -1;
    }

    printf("sendto: %d/n", bwrite);
    close( fd );
    return 0;
}

connect系统调用的执行流在到达myip4_datagram_connect函数之前,已经对本地端口号进行自动选择,并把socket绑定到了myudp_hash表中。到达myip4_datagram_connect函数之后,第一件事情是建立一个struct flowi:


struct flowi fl = {
    .oif   = 0,  //输出设备接口未定。
    .nl_u  = {.ip4_u = {.daddr              = 172.16.48.1  //目的地址。
                                    .saddr = 0.0.0.0      //源地址未定。
                                                 .tos = 0}},  //一般服务
    .proto = MY_IPPROTO_UDP,                                   // UDP协议
    .uli_u = {.ports = {.sport = 32768,     //自动选择的第一个源端口
                        .dport = 16000}}};  //目的端口
以该结构体为信息查询路由表,结果肯定查到main表,确定saddr为172.16.48.2。并得到一个struct rtable结构作为路由查询结果。

对于my_inet域的套接字,结构体struct socket有一个成员struct inet_sock sock代表网络层的一个套接字,其成员rcv_saddr(含义尚不明确)和saddr被赋172.16.48.2,daddr, dport被赋于服务器的地址和端口号。而表示连接状态的sk_state成员被赋于TCP_ESTABLISHED,这里需要注意的是TCP_ESTABLISHED并不专指TCP连接建立状态,所有执行connect成功的套接字,其状态都是TCP_ESTABLISHED。id被赋于当前时间。成员sk_dst_cache指向路由查询结果rtable的成员u.dst。从而套接字完全缓存路由查询的结果。

执行了connect后的socket,需要发送数据报时,关于对端的信息全部可以从socket本身得到。但需要重申的一点是:由于路由缓存的存在,在连接的socket上发送数据报并不会比在未连接的socket上发送数据报效率高多少。


接收一个UDP数据报


现在我们换一个角度,站在服务器的一方,看看如何被动地接收一个UDP数据,并作出相应的处理。我们把前面文章提及的UDP示例客户端放到172.16.48.1上,在主机172.16.48.2的eth0网络设备接口上再配一个从属IP地址172.16.48.13。让客户端向13发送UDP数据报。

函数myudp_rcv处理接收到的UDP数据报,该函数首先从数据报头中取出源和目的地址、端口。向哈希表myudp_hash查询,看是否存在相应的sokcet等待处理该数据报。如果没有,则发生错误,即对端主机向本机的某个特定端口发送了一个UDP数据,但本机并没有该端口标识的进程需要处理该数据报,所以,这是一个目的端口不可达错误。调用myicmp_send发送一个目的端口不可达出错报文。

目的端口不可达ICMP报文需要将整个源出错UDP报文作为数据负载(payload)放进报文中,所以,首先要对源UDP报文进行基本的正确性检查。还需要生成一份详细的ICMP参数信息,用于生成ICMP报文,ICMP参数信息用结构体struct icmp_bxm表示,下面是该结构体的定义:

struct icmp_bxm {
    struct sk_buff *skb;    //指向所在的sk_buff
    int offset;             //出错的源UDP报文在ICMP报文中的偏移量。
    int data_len;

    struct {
        struct icmphdr icmph;   //icmp头,需要填充完整。
        __u32          times[3];
    } data;
    int head_len;
    struct ip_options replyopts;    //ip选项。
    unsigned char  optbuf[40];
};

发送一个数据报,相应的,肯定需要一个套接字,为发送这个ICMP服文,协议栈为每个CPU创建了一个套接字,命名为myicmp_socket。


UDP通讯的两端


说起套接字的通讯,我们一般会想到C/S结构(客户端/服务端结构),从应用的角度来讲,确实如此,比如IM(即时通讯)服务,一台IM服务器同时与几万的IM客户端进行着UDP数据报的通讯。但我们从套接字的编程实现,以及TCP/IP协议栈的实现原理这一层上来看,并不存在着服务端与客户端的明显区别。

网络上的两台主机(为简化起见,我们假设它们处于同一子网内,并能互相连通),为了建立一个UDP的通讯,一端A必须事先知道另一端B的端口(B有一个进程可以接受UDP数据报)。即B必须先建立一个套接字,并自己为其选择一个固定的端口号,而不是让系统自动选择,并把这个端口号告知A(通过人,或者是熟知端口)。然后A就可以通过connect系统调用,在其socket上设置好相应参数,以后每个发出的数据包的UDP首部中总标明目的地址B和其相应的端口号;或者每发一个数据报,通过sendto传入B的地址和端口号,以确保每个发出的数据报在B端被正确的进程接收。

而A端可以自己选择一个固定的端口号,也可以由系统自动选择,这并不重要。因为B端收到来自A端的第一个数据报后,可以从UDP首部中判断A端的端口号,并在回应包中进行正确设置。在这样的情况下,我们一般就认为B是服务端,A是客户端。它们的唯一区别在于服务端的端口号必须是事先被客户端知道的。而客户端的端口号的选择则相对比较随便。

我们前面文章提到过协议栈中的函数myudp_v4_get_port可以为一个UDP套接口选择一个可以使用的端口号,也可以由我们自己选择一个端口号,然后由该函数判断是否可用。我们通过bind系统调用选择一个固定的端口号:

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
sockfd是套接字描述符,会被转化为相应的套接字结构体,my_addr和addrlen是要绑定的地址和地址长度。地址包括IP地址(标明主机)和端口号(标明进程)。

UDP协议没有自己的bind函数,直接使用了MY_INET域的通用bind函数。我们通常绑定的地址是INADDR_ANY,这是一个全零地址,表示对本机接收地址不作限制,即对端可以通过本机的任何一个网络设备接口向本进程发送UDP数据报,这个IP地址被赋给套接字结构体的成员rcv_saddr和saddr,这两个成员分别表示绑定的本机IP地址和发送数据报时的本机源IP地址。为INADDR_ANY,表示发送时根据对端IP地址,通过查询路由表获得本机源IP地址。但端口必须由应用程序指定,这样才使bind系统变得有意义,当然,也可以设定端口为0,让协议栈自动选择,但这样,bind调用不调用变得没有任何区别了(connect或myinet_sendmsg函数在发现端口没有绑定时,会执行自动绑定的)。对于端口绑定还有一个限制,即要绑定的端口小于1024(PROT_SOCK)时,用户必须有相应的权限才能执行绑定,因为小于1024的端口一般为系统服务保留。

最后,设置目的IP地址和目的端口为0,表示对对端不作任何限制。


发送一个UDP数据报


UDP是一个简单的面向数据报的运输层协议,我们先站在UDP客户端的角度来看看如何发送一个UDP数据报,以及协议栈为发送一个UDP数据报做了哪些事情。

UDP数据报可以在未连接的socket上发送(使用sendto系统调用,指定目的地址),也可以在已连接的socket上发送(使用send系统调用,不用指定目的地址),下面我们分两种情况讨论。

下面是一个在未连接的socket上发送UDP数据的用户态程序示例(注:该程序的格式和风格相当不好,只是为临时测试使用。),该程序目前还只管发送,不处理接收,关于接收,我们后面再作分析:

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>

#include <arpa/inet.h>
#include <unistd.h>
int main()
{
    int i;
    struct sockaddr_in dest;
    dest.sin_family = MY_PF_INET;
    dest.sin_port = htons(16000);
    dest.sin_addr.s_addr = 0x013010AC;  //目的地址是172.16.48.1(网络字节序)

    //创建UDP数据报服务的socket。
    int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
    if( fd < 0 ){
        perror("socket: ");
        return -1;
    }

    int bwrite = sendto( fd, "abcdefg", 7, 0, (struct sockaddr *)&dest, sizeof(dest) );
    if( bwrite == -1 ){
        perror("send: ");
        close(fd);
        return -1;
    }
    printf("sendto: %d/n", bwrite);
    close( fd );
    return 0;
}
创建socket的操作跟RAW协议的差不多,只有少数区别,内核中套接字结构上的操作集,协议名略有不同而已。我们重点看sendto操作所引发的内核代码执行。sendto所到达的my_inet模块的第一站是myinet_sendmsg,一般来讲,该函数只要调用udp协议自己的 udp_sendmsg即可,但在之前,它还有一样事情要完成,就是为这个socket执行绑定,这个绑定可能跟服务器端的bind系统调用有些区别。试想,如果我们用这个udp socket发送出去了一个数据报,但没有记录下这个udp socket,那等对端的回应数据报来的时候,我们就不知道哪个socket要接收这个数据报了。绑定就是记录这个udp socket。

myudp_hash是一个具有128项的哈希数组,每一项都是一个udp socket的链表,每个udp socket以自己的源地址端口号为哈希主键插入这个数组。源地址端口可以是用户自己指定的,也可以是由内核自动分配的。

内核自动分配的源端口号有一个范围,这个范围段似乎是由系统的内存大小决定的(具体有待进一步分析),如果内存大(似乎是有高端内存可用),范围段是 32768-61000,否则就是1024-4999。udp_port_rover是一个全局变量,初始值为范围段的下限,每次新分配端口,记录下分配的端口号,下一次再分配时,在前一次的基础上加1,然后查询对应的myudp_hash中的项,如果该项的链表不为空,则找下一项,直至遍历整个数组,如果为空,则分配成功。所以,当连续分配128个端口后(数组中的128项中,链表全不为空),这个查询必然失败,最后遍历数组完成时,得到的端口号必然是前一次分配的端口号加127。然后,端口号每次加128,再查询对应的数组项,看该端口号有没有被使用掉。

这个描述可能有点模糊,简单总结一下就是:每次分配一个端口号,先在前一次分配值的基础上以1为步进值递增,如果对应的哈希数组中的链表为空,则肯定没有被使用过,直接使用。如果遍历完整个哈希表都没有空的链表,则要查询链表中的每一项,以得到未使用的端口。

用户自己指定一个端口,则我们到对应的哈希数组中的链表查询,如果已被使用,并且不能重用,则分配端口号失败。对用户自己指定的端口,没有范围段的限制。这个一般用于服务端,而自动分配端口用于客户端。

上班时间到了,待续......


自动分配UDP本地端口


当建立一个UDP的socket用于网络通讯时,我们需要先为这个socket绑定一个本地端口号。因为端口在一台主机上是用于标识进程的,如果没有端口号,当收到来自对端主机的报文时,就不知道应该由哪一个进程来接收这个报文。但有时,我们建立UDP的sokcet以后,并不调用bind进行端口绑定,也能正常工作。这是因为协议栈对于没有进行端口绑定的socket进行了自动绑定。

在SOCK_DGRAM类型的套接字的操作函数集的sendmsg成员函数中,每次调用对应的第4层协议的sendmsg成员函数时,都会进行端口号的检查,如果没有绑定就调用协议的成员函数get_port进行自动绑定。代表INET域网络层套接字的结构体struct inet_sock有两个端口号相关的成员__u16 num和__u16 sport。它们都代表套接字的本地端口号。num是主机字节序,sport是网络字节序。当套接字类型为SOCK_RAW时,它们代表的是协议号(icmp,igmp等)。套接字层的sendmsg检查端口号绑定时,就是查看num是否为零。

udp协议提供udp_v4_get_port函数用于自动获取本地端口号。端口号有一个固定的数值范围,自动获取必须在这个范围内进行。数组int sysctl_local_port_range[2]指定了本地端口号的范围。其默认值为1024到4999。对于高可用性系统,它的值应该是32768到61000(在TCP协议进行初始化时,会进行这项设置)。可通过修改文件/proc/sys/net/ipv4/ip_local_port_range的内容来修改这个范围。

udp_hash是一个list数组,总共有128项,所有在协议栈中建立的udp socket全部以本地端口号为关键字被放入这个哈希数组中,全局变量udp_port_rover记录了最近一次被分配的端口号。寻找一个新的可用的端口,总是从udp_port_rover开始找,检查udp_hash[udp_port_rover & (UDP_HTABLE_SIZE - 1)]的list是否为空,如果为空,则取udp_port_rover为新的端口,如果不为空,则记录下这个list的size,同时保存下该端口号,然后遍历整个数组,找到size最小的一个list,取对应的端口号为我们所要获得的端口。然后,检查这个新获得的端口号是否已经被使用(同样,通过检查udp_hash实现)。如果已在使用中,则把端口号加上UDP_HTABLE_SIZE(128)再检查。直至获得未使用的端口号。


Connect系统调用


下面是connect系统调用的函数原型:

#include <sys/types.h>
#include <sys/socket.h>
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen );
man手册里面这样描述该系统调用:connect()系统调用把由文件描述符sockfd所代表的套接字连接到serv_addr所指定的地址上,参数addrlen用于标明serv_addr的大小。如果sockfd是SOCK_DGRAM类型的套接字,那么serv_addr就是数据报文缺省传向的那个地址,同时,只有来自该地址的数据报文才能被

该socket接收到。如果socket的类型是SOCK_STREAM或者SOCK_SEQPACKET,那么这个调用将试图同绑定在地址serv_addr的socket建立一个连接。一般来讲,面向连接协议的socket只可能一次成功调用connect,而无连接协议的socket可以多次使用connect改变它们的连接关系,同时,无连接协议的socket可以通过设置serv_addr的成员sa_family为AF_UNSPEC来解除已有的连接。

在TCP/IP的源代码实现中,SOCK_DGRAM套接字类型的connect函数是inet_dgram_connect。该函数首先检查connect系统调用的serv_addr参数,如果它的sa_family成员的值是AF_UNSPEC,则表示解除该socket已有的连接,直接调用具体协议的disconnect函数,UDP协议的disconnect函数是udp_disconnect,该函数所要做的工作主要是重置struct inet_sock(表示是INET域的socket)的几个成员,sk_state置TCP_CLOSE;daddr,dport置0,以清目的地址和端口;sk_bound_dev_if清零,表示清输入网络设备接口;然后选择性地清源地址和源端口。然后清目的入口(struct dst_entry)。否则,在判定本地端口已被绑定之后,执行协议的connect函数,udp协议的connect函数是ip4_datagram_connect。


发送一个UDP数据报


在Socket编程中,发送数据报文可供使用的API函数有send,sendto和sendmsg,下面是关于前两个系统调用的原型:

#include <sys/socket.h>
ssize_t send( int socket, const void *buffer, size_t length, int flags );
请注意它的返回值的类型ssize_t,其含义是signed size。从内核代码中,我们可以看到,在32位系统上,它是int,在64位系统上,它是long。它常用于表示在某一次操作后,缓冲区中可以被读或写的字节数量。相对应的,还有一个数据类型size_t,其含义是unsigned size。常用于表示对象本身的大小,操作sizeof的返回值就是该类型,malloc,memcpy等函数的参数中用该类型表示对象的大小,在32位系统上,它是unsigned int,在64位系统上,它是unsigned long。

send执行成功,会返回被发送出去的数据报文的字节数,如果执行失败,则会返回-1(所以不能返回size_t类型),并且可以从errno上查找到错误原因。

#include <sys/socket.h>
ssize_t sendto(int socket, const void *message, size_t length,
                 int flags, const struct sockaddr *dest_addr,
                  socklen_t dest_len);

在内核的实现中,send和sendto系统调用最终都会调用到内核函数:

asmlinkage long sys_sendto(int fd, void __user * buff, size_t len, unsigned flags,
       struct sockaddr __user *addr, int addr_len)

在send系统调用中,参数addr被置为NULL,addr_len为0。sys_sendto首先根据传入的描述符fd,找到对应的struct socket结构体。然后构建内核的消息结构struct msghdr:

struct msghdr {
   void * msg_name;
   int   msg_namelen;
   struct iovec * msg_iov;
   __kernel_size_t msg_iovlen;
   void   * msg_control;
   __kernel_size_t msg_controllen;
   unsigned msg_flags;
};

msg_name和msg_namelen就是数据报文要发向的对端的地址信息(即sendto系统调用中的addr和addr_len)。当使用send时,它们的值为NULL和0。msg_iov的定义如下:

struct iovec
{
   void __user *iov_base;
   __kernel_size_t iov_len;
};

表示存放待发送数据的一个缓冲区,iov_base是缓冲区的起始地址,指向message, iov_len是缓冲区的长度,指向length。msg_iovlen是缓冲区的数量,对于sendto和send来讲,msg_iovlen都是1。msg_flags即为传入的参数flags,现在暂时不过多的关注flags的应用。msg_control和msg_controllen暂时不关注。

sys_sendto构建完这些后,调用sock_sendmsg继续执行发送流程,传入参数为struct msghdr和数据的长度。忽略中间的一些不重要的细节,sock_sendmsg继续调用__sock_sendmsg,__sock_sendmsg最后调用struct socket->ops->sendmsg,即对应套接字类型的sendmsg函数,所有的套接字类型的sendmsg函数都是inet_sendmsg,该函数首先检查本地端口是否已绑定,无绑定则执行自动绑定,而后调用具体协议的sendmsg函数。

下面再来看sendmsg系统调用:

#include <sys/socket.h>
ssize_t sendmsg(int socket, const struct msghdr *message, int flags);

可以看到,它跟send和sendto的最大区别就是struc msghdr由用户来构建完成,对应的内核处理函数是sys_sendmsg。


从套接字上得到扩展的更为可靠的出错信息


在前一篇中,我们提到在对端主机上没有创建指定的UDP套接字时,我们向其发送一个UDP包,会得到一个目的端口不可达的ICMP出错报文。但内核在处理完该报文后,给应用程序仅仅返回一个ECONNREFUSED错误号,所以应用程序能知道的全部信息就是连接被拒绝,至于为什么被拒绝,没有办法知道。我们可以通过套接字选项的设置,让内核返回更为详细的出错信息,以利于调试程序,发现问题。下面是通过套接字选项传递扩展出错信息的一个示例程序。关于内核原理的分析,在下一篇给出。


/*************************************************************************
    > File Name: format.c
    > Author: ngkimbing
    > Mail: ngkimbing@foxmail.com
    > Created Time: Sat 14 Mar 2020 06:34:51 PM CST
 ************************************************************************/

#include "my_inet.h"
#include <errno.h>
#include <linux/errqueue.h>
#include <linux/types.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>

#include <arpa/inet.h>
#include <unistd.h>


int ip_control_msg(struct cmsghdr *msg) {
    int ret = 0;
    switch (msg->cmsg_type) {
        case IP_RECVERR: {
            struct sock_extended_err *exterr;
            exterr = (struct sock_extended_err *)(CMSG_DATA(msg));
            printf("ee_errno: %u/n", exterr->ee_errno);
            printf("ee_origin: %u/n", exterr->ee_origin);
            printf("ee_type: %u/n", exterr->ee_type);
            printf("ee_code: %u/n", exterr->ee_code);
            printf("ee_pad: %u/n", exterr->ee_pad);
            printf("ee_info: %u/n", exterr->ee_info);
            printf("ee_data: %u/n", exterr->ee_data);
        }
            ret = -1;
            break;
        default: break;
    }
    return ret;
}
int control_msg(struct msghdr *msg) {
    int             ret         = 0;
    struct cmsghdr *control_msg = CMSG_FIRSTHDR(msg);
    while (control_msg != NULL) {
        switch (control_msg->cmsg_level) {
            case SOL_IP: ret = ip_control_msg(control_msg); break;
            default: break;
        }
        control_msg = CMSG_NXTHDR(msg, control_msg);
    }
    return ret;
}

int main() {
    int                i;
    struct sockaddr_in dest;
    dest.sin_family      = MY_PF_INET;
    dest.sin_port        = htons(16000);
    dest.sin_addr.s_addr = 0x013010AC;

    int fd = socket(MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP);
    if (fd < 0) {
        perror("socket: ");
        return -1;
    }
    if (connect(fd, (struct sockaddr *)&dest, sizeof(dest)) < 0) {
        perror("connect: ");
        return -1;
    }

    int val = 1;
    if (setsockopt(fd, SOL_IP, IP_RECVERR, &val, sizeof(val)) == -1) {
        perror("setsockopt: ");
        return -1;
    }

    int bwrite = send(fd, "abcdefg", 7, 0);
    if (bwrite == -1) {
        perror("send: ");
        return -1;
    }
    char          buf[1024];
    char          control_buf[1024];
    struct msghdr msg;
    struct iovec  iov = {buf, 1024};
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov        = &iov;
    msg.msg_iovlen     = 1;
    msg.msg_control    = &control_buf;
    msg.msg_controllen = 1024;

    int bread = recvmsg(fd, &msg, MSG_ERRQUEUE);
    if (bread == -1) {
        perror("recv: ");
        return -1;
    }
    if (control_msg(&msg) >= 0)
        printf("successed!/n");
    else
        printf("failed!/n");

    close(fd);
    return 0;
}

执行结果:

ee_errno: 111           //ECONNREFUSED
ee_origin: 2               //SO_EE_ORIGIN_ICMP
ee_type: 3                 //目的不可达
ee_code: 3               //端口不可达
ee_pad: 0
ee_info: 0
ee_data: 0
failed!


从套接字上得到扩展的更为可靠的出错信息(续)


接着前一篇,我们来看这个应用程序背后,内核真正做了一些什么事情。

代表MY_INET域套接字的结构体struct inet_sock有一个成员recverr,它占1bit长度,可能的取值是1或0,当为0时表示socket上出错时,只通过系统调用向应用程序返回错误号,不提供进一步的详细信息。当取值为1时,则表示socket上出错时,则向struct inet_sock的成员sk_error_queue(一个sk_buff的队列)存入一个特殊的struct sk_buff,在sk_buff的成员cb中放入详细的错误信息,应用程序通过特定的系统调用可以取得详细的出错信息。

recverr的值可以通过套接字选项操作进行设置,它是一个IP层的选项,对应的选项名是IP_RECVERR。下面的代码就是将它的值设为1(打开选项):

int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 )
    ;//deal with error

当打开了这个选项后,我们在该socket上发送UDP数据报,按照前面文章提及的测试环境运行,172.16.48.2继续会收到ICMP目的不可达报文,在差错数据报处理时,会达到函数myudp_err,该函数会设置socket的成员sk_err,同时,它也会检查recverr成员,如果为1,则要在sk_error_queue队列中放入一个特殊的出错信息sk_buff。该sk_buff保留了出错的那个源UDP数据报,同时在它的cb成员中保存了一个结构体struct sock_exterr_skb,该结构体记录了详细的出错信息,下面是其定义:

struct sock_exterr_skb
{
    union {
    struct inet_skb_parm    h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
    struct inet6_skb_parm   h6;
#endif
    } header;
    struct sock_extended_err    ee;
    u16             addr_offset;
    u16             port;
};

addr_offset和port是出错UDP数据报的地址和端口号,ee的定义如下:

struct sock_extended_err
{
    __u32   ee_errno;       //错误号。
    __u8    ee_origin;      //产生错误的源,我们的环境下,产生错误的源为一个ICMP包。
    __u8    ee_type;        //ICMP类型。
    __u8    ee_code;        //ICMP代码。
    __u8    ee_pad;
    __u32   ee_info;        //用于EMSGSIZE时找到的MTU。
    __u32   ee_data;
};

我们保存了出错信息,应用程序要取得这个出错信息,必须使用特定的系统调用,recvmsg可以获得详细的出错信息,同时,调用接口上必须使用标志MSG_ERRQUEUE表示取错误队列,下面是recvmsg的定义:

ssize_t recvmsg(int s, struct msghdr *msg, int flags);

flags置MSG_ERRQUEUE,msg结构控制信息成员msg_control和msg_controllen需要分配一个缓存,用于辅助信息的传递。关于接收,可以查看前面一篇的源代码和man recvmsg,这里不再重复。


用于表示socket的结构体(1)


用户使用socket系统调用编写应用程序时,通过一个数字来表示一个socket,所有的操作都在该数字上进行,这个数字称为套接字描述符。在系统调用的实现函数里,这个数字就会被映射成一个表示socket的结构体,该结构体保存了该socket的所有属性和数据。在内核的协议中实现中,关于表示socket的结构体,是一个比较复杂的东西,下面一一介绍。

struct socket。

这是一个基本的BSD socket,我们调用socket系统调用创建的各种不同类型的socket,开始创建的都是它,到后面,各种不同类型的socket在它的基础上进行各种扩展。struct socket是在虚拟文件系统上被创建出来的,可以把它看成一个文件,是可以被安全地扩展的。下面是其完整定义:

    struct socket {
        socket_state            state;
        unsigned long           flags;
        const struct proto_ops  *ops;
        struct fasync_struct    *fasync_list;
        struct file             *file;
        struct sock             *sk;
        wait_queue_head_t       wait;
        short                   type;
    };

state用于表示socket所处的状态,是一个枚举变量,其类型定义如下:

    typedef enum {
        SS_FREE = 0,            //该socket还未分配
        SS_UNCONNECTED,         //未连向任何socket
        SS_CONNECTING,          //正在连接过程中
        SS_CONNECTED,           //已连向一个socket
        SS_DISCONNECTING        //正在断开连接的过程中
    }socket_state;

该成员只对TCP socket有用,因为只有tcp是面向连接的协议,udp跟raw不需要维护socket状态。
flags是一组标志位,在内核中并没有发现被使用。
ops是协议相关的一组操作集,结构体struct proto_ops的定义如下:


struct proto_ops {
    int            family;
    struct module *owner;
    int (*release)(struct socket *sock);
    int (*bind)(struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
    int (*connect)(struct socket *  sock,
                   struct sockaddr *vaddr,
                   int              sockaddr_len,
                   int              flags);
    int (*socketpair)(struct socket *sock1, struct socket *sock2);
    int (*accept)(struct socket *sock, struct socket *newsock, int flags);
    int (*getname)(struct socket *  sock,
                   struct sockaddr *addr,
                   int *            sockaddr_len,
                   int              peer);
    unsigned int (*poll)(struct file *             file,
                         struct socket *           sock,
                         struct poll_table_struct *wait);
    int (*ioctl)(struct socket *sock, unsigned int cmd, unsigned long arg);
    int (*listen)(struct socket *sock, int len);
    int (*shutdown)(struct socket *sock, int flags);
    int (*setsockopt)(struct socket *sock,
                      int            level,
                      int            optname,
                      char __user *optval,
                      int          optlen);
    int (*getsockopt)(struct socket *sock,
                      int            level,
                      int            optname,
                      char __user *optval,
                      int __user *optlen);
    int (*sendmsg)(struct kiocb * iocb,
                   struct socket *sock,
                   struct msghdr *m,
                   size_t         total_len);
    int (*recvmsg)(struct kiocb * iocb,
                   struct socket *sock,
                   struct msghdr *m,
                   size_t         total_len,
                   int            flags);
    int (*mmap)(struct file *          file,
                struct socket *        sock,
                struct vm_area_struct *vma);
    ssize_t (*sendpage)(struct socket *sock,
                        struct page *  page,
                        int            offset,
                        size_t         size,
                        int            flags);
};

协议栈中总共定义了三个strcut proto_ops类型的变量,分别是myinet_stream_ops, myinet_dgram_ops, myinet_sockraw_ops,对应流协议, 数据报和原始套接口协议的操作函数集。

type是socket的类型,对应的取值如下

    enum sock_type {
        SOCK_DGRAM  = 1,
        SOCK_STREAM = 2,
        SOCK_RAW    = 3,
        SOCK_RDM    = 4,
        SOCK_SEQPACKET  = 5,
        SOCK_DCCP   = 6,
        SOCK_PACKET = 10,
    };

sk是网络层对于socket的表示,结构体struct sock比较庞大,这里不详细列出,只介绍一些重要的成员,

sk_prot和sk_prot_creator,这两个成员指向特定的协议处理函数集,其类型是结构体struct proto,该结构体也是跟struct proto_ops相似的一组协议操作函数集。这两者之间的概念似乎有些混淆,可以这么理解,struct proto_ops的成员操作struct socket层次上的数据,处理完了,再由它们调用成员sk->sk_prot的函数,操作struct sock层次上的数据。即它们之间存在着层次上的差异。struct proto类型的变量在协议栈中总共也有三个,分别是mytcp_prot,myudp_prot,myraw_prot,对应TCP, UDP和RAW协议。

sk_state表示socket当前的连接状态,是一个比struct socket的state更为精细的状态,其可能的取值如下:

enum{}
        TCP_ESTABLISHED = 1,
        TCP_SYN_SENT,
        TCP_SYN_RECV,
        TCP_FIN_WAIT1,
        TCP_FIN_WAIT2,
        TCP_TIME_WAIT,
        TCP_CLOSE,
        TCP_CLOSE_WAIT,
        TCP_LAST_ACK,
        TCP_LISTEN,
        TCP_CLOSING,
 
        TCP_MAX_STATES
    };

这些取值从名字上看,似乎只使用于TCP协议,但事实上,UDP和RAW也借用了其中一些值,在一个socket创建之初,其取值都是TCP_CLOSE,一个UDP socket connect完成后,将这个值改为TCP_ESTABLISHED,最后,关闭sockt前置回TCP_CLOSE,RAW也一样。

sk_rcvbuf和sk_sndbuf分别表示接收和发送缓冲区的大小。sk_receive_queue和sk_write_queue分别为接收缓冲队列和发送缓冲队列,队列里排列的是套接字缓冲区struct sk_buff,队列中的struct sk_buff的字节数总和不能超过缓冲区大小的设定。


用于表示socket的结构体(2)


接着上一篇,继续介绍struct sock。

sk_rmem_alloc, sk_wmem_alloc和sk_omem_alloc分别表示接收缓冲队列,发送缓冲队列及其它缓冲队列中已经分配的字节数,用于跟踪缓冲区的使用情况。

struct sock有一个struct sock_common成员,因为struct inet_timewait_sock也要用到它,所以把它单独归到一个结构体中,其定义如下:

struct sock_common {
    unsigned short         skc_family;
    volatile unsigned char skc_state;
    unsigned char          skc_reuse;
    int                    skc_bound_dev_if;
    struct hlist_node      skc_node;
    struct hlist_node      skc_bind_node;
    atomic_t               skc_refcnt;
    unsigned int           skc_hash;
    struct proto *         skc_prot;
};

struct inet_sock。
这是INET域专用的一个socket表示,它是在struct sock的基础上进行的扩展,在基本socket的属性已具备的基础上,struct inet_sock提供了INET域专有的一些属性,比如TTL,组播列表,IP地址,端口等,下面是其完整定义:

struct inet_sock {
    struct sock sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
    struct ipv6_pinfo *pinet6;
#endif
    __u32 daddr;                // IPv4的目的地址。
    __u32 rcv_saddr;            // IPv4的本地接收地址。
    __u16              dport;   //目的端口。
    __u16              num;     //本地端口(主机字节序)。
    __u32              saddr;   //发送地址。
    __s16              uc_ttl;  //单播的ttl。
    __u16              cmsg_flags;
    struct ip_options *opt;
    __u16              sport;  //源端口。
    __u16 id;      //单调递增的一个值,用于赋给iphdr的id域。
    __u8  tos;     //服务类型。
    __u8  mc_ttl;  //组播的ttl
    __u8  pmtudisc;
    __u8  recverr : 1, is_icsk : 1, freebind : 1,
        hdrincl : 1,  //是否自己构建ip首部(用于raw协议)
        mc_loop : 1;  //组播是否发向回路。
    int   mc_index;   //组播使用的本地设备接口的索引。
    __u32 mc_addr;    //组播源地址。
    struct ip_mc_socklist *mc_list;  //组播组列表。
    struct {
        unsigned int       flags;
        unsigned int       fragsize;
        struct ip_options *opt;
        struct rtable *    rt;
        int                length;
        u32                addr;
        struct flowi       fl;
    } cork;
};

struct raw_sock
这是RAW协议专用的一个socket的表示,它是在struct inet_sock基础上的扩展,因为RAW协议要处理ICMP协议的过滤设置,其定义如下:

struct raw_sock {
    struct inet_sock   inet;
    struct icmp_filter filter;
};

struct udp_sock
这是UDP协议专用的一个socket表示,它是在struct inet_sock基础上的扩展,其定义如下

struct udp_sock {
        struct inet_sock inet;
        int             pending;
        unsigned int    corkflag;
        __u16           encap_type;
        __u16           len;
    };

struct inet_connection_sock

看完上面两个,我们觉得第三个应该就是struct tcp_sock了,但事实上,struct tcp_sock并不直接从struct inet_sock上扩展,而是从struct inet_connection_sock基础上进行扩展,struct inet_connection_sock是所有面向连接的socket的表示,关于该socket,及下面所有tcp相关的socket,我们在分析tcp实现时再详细介绍,这里只列出它们的关系。

strcut tcp_sock
这是TCP协议专用的一个socket表示,它是在struct inet_connection_sock基础进行扩展,主要是增加了滑动窗口协议,避免拥塞算法等一些TCP专有属性。

struct inet_timewait_sock

struct tcp_timewait_sock
在struct inet_timewait_sock的基础上进行扩展。

struct inet_request_sock

struct tcp_request_sock
在struct inet_request_sock的基础上进行扩展。


创建一个socket



一个socket代表了通信链路的一端,存储或指向与链路有关的所有信息。Linux提供了创建socket的一个系统调用,通过该系统调用,能够得到一个用来访问套接字的描述符:

#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, int type, int protocol );

内核中的系统调用函数原型是在net/socket.c 1180行:

asmlinkage long sys_socket( int family, int type, int protocol );

该函数主要做了两件事情:创建一个代表通讯端点的结构体struct socket,将这个结构映射到一个文件描述符上,最后将这个描述符返回,也就是我们调用socket得到的套接字描述符。

下面是Linux内核中对结构socket的定义(不同操作系统间,对该结构的定义会有差异):

 struct socket {
     socket_state            state;
     unsigned long           flags;
     struct proto_ops        *ops;
     struct fasync_struct    *fasync_list;
     struct file             *file;
     struct sock             *sk;
     wait_queue_head_t       wait;
     short                   type;
 };

state是一个内部状态标志:

typedef enum {
    SS_FREE = 0,     /* 未分配 */
    SS_UNCONNECTED,  /* 未连接 */
    SS_CONNECTING,   /* 正在连接当中 */
    SS_CONNECTED,    /* 已经连向一个套接字 */
    SS_DISCONNECTING /* 正在断开连接 */
} socket_state;

flags也是一个标志,下面是它的取值:

#define SOCK_ASYNC_NOSPACE 0
#define SOCK_ASYNC_WAITDATA 1
#define SOCK_NOSPACE  2
#define SOCK_PASSCRED  3

ops是协议相关的一系例操作的集合,包括listen, bind, connect等常用socket操作,struct proto_ops结构体在include/linux/net.h 123行。

fasync_list是一个异步唤醒的列表,结构体struct fasync_struct在include/linux/fs.h 733行

sk是一个网络层的套接字表示,关于结构体struct sock,下文会有专门介绍。
type是套接字的类型:

enum sock_type {
   SOCK_STREAM = 1,        /*可靠字节流服务套接字,TCP*/
   SOCK_DGRAM = 2,  /*传输层数据报服务, UDP*/
   SOCK_RAW = 3,  /*网络层数据报服务, ICMP, IGMP,  原始IP*/
   SOCK_RDM = 4,  /*可靠的数据报服务*/
   SOCK_SEQPACKET = 5, /*可靠的双向记录流服务*/
   SOCK_PACKET = 10,  /*已废弃*/
};

暂时放一下struct sock,先来看看sys_socket的第一步创建struct socket中究竟做了些什么(描述越过了一些不是很重要的步骤):

首先,检查传入的用来标识域的协议族变量family是否在合法范围内,关于family,我们只关心其中的几个值,PF_INET表示因特网协议,PF_UNIX是unix文件系统套接字。

然后,对于(family == PF_INET && type == SOCK_PACKET )的情况,因为是已废弃的,给出警告信息。

net_families是一个数组,所有的协议族都在这个数组中注册,数组的项是一个结构体:

struct net_proto_family {
   int  family;
   int  (*create)(struct socket *sock, int protocol);
   short  authentication;
   short  encryption;
   short  encrypt_net;
   struct module *owner;
};

对于我们要创建的family,我们必须确保能在这个数组中找到相应的项(即内核支持该域)。

在内存中创建一个struct socket,并将其type赋值为传入的type值。

调用net_families[family]->create完成最后的创建工作。返回。

至此,一个socket就创建成功了。但还有两个问题没有明确:struct sock结构体的内容,以及net_families[family]->create如何完成对socket的创建。下一篇将结合inet域的实际例子进行分析。


struct sock详解


结构体sock是套接口在网络层的表示,在代码include/net/sock.h 174行定义,下面是其内容:

struct sock {
    struct sock_common __sk_common;
#define sk_family __sk_common.skc_family
#define sk_state __sk_common.skc_state
#define sk_reuse __sk_common.skc_reuse
#define sk_bound_dev_if __sk_common.skc_bound_dev_if
#define sk_node __sk_common.skc_node
#define sk_bind_node __sk_common.skc_bind_node
#define sk_refcnt __sk_common.skc_refcnt
    unsigned char       sk_shutdown : 2, sk_no_check : 2, sk_userlocks : 4;
    unsigned char       sk_protocol;
    unsigned short      sk_type;
    int                 sk_rcvbuf;
    socket_lock_t       sk_lock;
    wait_queue_head_t * sk_sleep;
    struct dst_entry *  sk_dst_cache;
    struct xfrm_policy *sk_policy[2];
    rwlock_t            sk_dst_lock;
    atomic_t            sk_rmem_alloc;
    atomic_t            sk_wmem_alloc;
    atomic_t            sk_omem_alloc;
    struct sk_buff_head sk_receive_queue;
    struct sk_buff_head sk_write_queue;
    int                 sk_wmem_queued;
    int                 sk_forward_alloc;
    unsigned int        sk_allocation;
    int                 sk_sndbuf;
    int                 sk_route_caps;
    int                 sk_hashent;
    unsigned long       sk_flags;
    unsigned long       sk_lingertime;

    struct {
        struct sk_buff *head;
        struct sk_buff *tail;
    } sk_backlog;
    struct sk_buff_head sk_error_queue;
    struct proto *      sk_prot;
    struct proto *      sk_prot_creator;
    rwlock_t            sk_callback_lock;
    int                 sk_err, sk_err_soft;
    unsigned short      sk_ack_backlog;
    unsigned short      sk_max_ack_backlog;
    __u32               sk_priority;
    struct ucred        sk_peercred;
    int                 sk_rcvlowat;
    long                sk_rcvtimeo;
    long                sk_sndtimeo;
    struct sk_filter *  sk_filter;
    void *              sk_protinfo;
    struct timer_list   sk_timer;
    struct timeval      sk_stamp;
    struct socket *     sk_socket;
    void *              sk_user_data;
    struct page *       sk_sndmsg_page;
    struct sk_buff *    sk_send_head;
    __u32               sk_sndmsg_off;
    int                 sk_write_pending;
    void *              sk_security;
    void (*sk_state_change)(struct sock *sk);
    void (*sk_data_ready)(struct sock *sk, int bytes);
    void (*sk_write_space)(struct sock *sk);
    void (*sk_error_report)(struct sock *sk);
    int (*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb);
    void (*sk_destruct)(struct sock *sk);
};

__sk_common是套接口在网络层的最小表示。下面是其定义:

struct sock_common {
    unsigned short      skc_family;         /*地址族*/
    volatile unsigned char  skc_state;      /*连接状态*/
    unsigned char       skc_reuse;          /*SO_REUSEADDR设置*/
    int         skc_bound_dev_if;
    struct hlist_node   skc_node;
    struct hlist_node   skc_bind_node;      /*哈希表相关*/
    atomic_t        skc_refcnt;             /*引用计数*/
};
  • sk_shutdown是一组标志位,SEND_SHUTDOWN and/or RCV_SHUTDOWN。
  • sk_userlocks, SO_SNDBUF and SO_RCVBUF。
  • sk_rcvbuf表示接收缓冲区的字节长度。
  • sk_rmem_alloc表示接收队列已提交的字节数。
  • sk_receive_queue表示接收的数据包的队列。
  • sk_wmem_alloc表示发送队列已提交的字节数。
  • sk_write_queue表示发送数据包的队列。
  • sk_sndbuf表示发送缓冲区的字节长度。
  • sk_flags,SO_LINGER (l_onoff),SO_BROADCAST,SO_KEEPALIVE,SO_OOBINLINE。
  • sk_prot是指定的域内部的协议处理函数集,它是套接口层跟传输层之间的一个接口,提供诸如bind, accept, close等操作。
  • sk_ack_backlog表示当前的侦听队列。
  • sk_max_ack_backlog表示最大的侦听队列。
  • sk_type表示套接字的类型,如SOCK_STREAM。
  • sk_protocol表示在当前域中套接字所属的协议。
  • 几个函数指针均属回调函数,分别在套接口状态变化,有数据到达需要处理,有发送空间可用,有错误等时候被回调。最后一个函数sk_destruct在套接口释放时被回调。


inet_create如何完成对socket的创建


前面讲到在sys_socket函数中,有一步是调用net_families[family]->create完成最后的创建工作,下面就以inet域的创建来解释这最后一步的创建工作:

  1. 设socket->state = SS_UNCONNECTED。

  2. 从数组inetsw中匹配套接字类型和协议类型。inetsw是一个链表数组,也就是说数组的每一项是一个链表,同套接字类型的在同一个链表中。比如,用户这样创建一个TCP协议的套接字:

    socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )
    

    最终,内核在inetsw中匹配到的是这样一个结构体:

    static struct inet_protosw inetsw_array[] = {{
        .type       = SOCK_STREAM,
        .protocol   = IPPROTO_TCP,
        .prot       = &tcp_prot,
        .ops        = &inet_stream_ops,
        .capability = -1,
        .no_check   = 0,
        .flags      = INET_PROTOSW_PERMANENT,
    }}
    

    这里最关键的是prot成员和ops成员,tcp_prot将提供TCP协议相关的全部操作,inet_stream_ops将提供域相关的全部操作,包括listen, connect等。

  3. socket->ops = 匹配到的那个ops。

  4. 分配socket->sock。

  5. 让struct inet_sock指向socket->sock,struct inet_sock是struct sock的超集。其头部内容即为struct sock。

  6. 为inet_sock和socket->sock的成员赋初始值。这里,我们可以看到一些平时我们比较关心的问题,比如:inet->mc_ttl = 1。

  7. 调用socket->sock->sk_prot->init(...)完成整个创建过程。

最后,调用sock_map_fd找一个空闲的文件描述符,映射到这个创建好的套接字上,将这个文件描述符返回。

描述到这里,一个最为粗糙的socket创建过程算是完成了,但留下的问题却更多了,它涉及到很多宠大的结构体,其内容与socket的正常工作都息息相关,下文会陆续给出分析。

struct sock详解

结构体sock是套接口在网络层的表示,在代码include/net/sock.h 174行定义,下面是其内容:

    struct sock {
        struct sock_common  __sk_common;
#define sk_family       __sk_common.skc_family
#define sk_state        __sk_common.skc_state
#define sk_reuse        __sk_common.skc_reuse
#define sk_bound_dev_if     __sk_common.skc_bound_dev_if
#define sk_node         __sk_common.skc_node
#define sk_bind_node        __sk_common.skc_bind_node
#define sk_refcnt       __sk_common.skc_refcnt
        unsigned char       sk_shutdown : 2,
                            sk_no_check : 2,
                            sk_userlocks : 4;
        unsigned char       sk_protocol;
        unsigned short      sk_type;
        int         sk_rcvbuf;
        socket_lock_t       sk_lock;
        wait_queue_head_t   *sk_sleep;
        struct dst_entry    *sk_dst_cache;
        struct xfrm_policy  *sk_policy[2];
        rwlock_t        sk_dst_lock;
        atomic_t        sk_rmem_alloc;
        atomic_t        sk_wmem_alloc;
        atomic_t        sk_omem_alloc;
        struct sk_buff_head sk_receive_queue;
        struct sk_buff_head sk_write_queue;
        int         sk_wmem_queued;
        int         sk_forward_alloc;
        unsigned int        sk_allocation;
        int         sk_sndbuf;
        int         sk_route_caps;
        int         sk_hashent;
        unsigned long       sk_flags;
        unsigned long           sk_lingertime;
 
        struct {
            struct sk_buff *head;
            struct sk_buff *tail;
        } sk_backlog;
        struct sk_buff_head sk_error_queue;
        struct proto        *sk_prot;
        struct proto        *sk_prot_creator;
        rwlock_t        sk_callback_lock;
        int         sk_err,
                    sk_err_soft;
        unsigned short      sk_ack_backlog;
        unsigned short      sk_max_ack_backlog;
        __u32           sk_priority;
        struct ucred        sk_peercred;
        int         sk_rcvlowat;
        long            sk_rcvtimeo;
        long            sk_sndtimeo;
        struct sk_filter        *sk_filter;
        void            *sk_protinfo;
        struct timer_list   sk_timer;
        struct timeval      sk_stamp;
        struct socket       *sk_socket;
        void            *sk_user_data;
        struct page     *sk_sndmsg_page;
        struct sk_buff      *sk_send_head;
        __u32           sk_sndmsg_off;
        int         sk_write_pending;
        void            *sk_security;
        void            (*sk_state_change)(struct sock *sk);
        void            (*sk_data_ready)(struct sock *sk, int bytes);
        void            (*sk_write_space)(struct sock *sk);
        void            (*sk_error_report)(struct sock *sk);
        int             (*sk_backlog_rcv)(struct sock *sk,
                        struct sk_buff *skb);
        void            (*sk_destruct)(struct sock *sk);
    };
 
             __sk_common是套接口在网络层的最小表示。下面是其定义:
    struct sock_common {
        unsigned short      skc_family;         /*地址族*/
        volatile unsigned char  skc_state;      /*连接状态*/
        unsigned char       skc_reuse;          /*SO_REUSEADDR设置*/
        int         skc_bound_dev_if;
        struct hlist_node   skc_node;
        struct hlist_node   skc_bind_node;      /*哈希表相关*/
        atomic_t        skc_refcnt;             /*引用计数*/
    };
  • sk_shutdown是一组标志位,SEND_SHUTDOWN and/or RCV_SHUTDOWN。
  • sk_userlocks, SO_SNDBUF and SO_RCVBUF。
  • sk_rcvbuf表示接收缓冲区的字节长度。
  • sk_rmem_alloc表示接收队列已提交的字节数。
  • sk_receive_queue表示接收的数据包的队列。
  • sk_wmem_alloc表示发送队列已提交的字节数。
  • sk_write_queue表示发送数据包的队列。
  • sk_sndbuf表示发送缓冲区的字节长度。
  • sk_flags,SO_LINGER (l_onoff),SO_BROADCAST,SO_KEEPALIVE,SO_OOBINLINE。
  • sk_prot是指定的域内部的协议处理函数集,它是套接口层跟传输层之间的一个接口,提供诸如bind, accept, close等操作。
  • sk_ack_backlog表示当前的侦听队列。
  • sk_max_ack_backlog表示最大的侦听队列。
  • sk_type表示套接字的类型,如SOCK_STREAM。
  • sk_protocol表示在当前域中套接字所属的协议。
  • 几个函数指针均属回调函数,分别在套接口状态变化,有数据到达需要处理,有发送空间可用,有错误等时候被回调。最后一个函数sk_destruct在套接口释放时被回调。


创建一个套接字


我们已经完成了MY_PF_INET域的初始化,虽然留了很多空,但我们至少已经具备了:TCP, UDP, RAW三种协议;TCP, UDP, ICMP, IGMP四种基本协议;inetsw数组。有了这些,我们可以尝试着创建一个套接字试试。

关于套接字创建的执行流程,前文已有描述,其最终会进入我们的family中的创建函数:

static int myinet_create(struct socket *sock, int protocol);
套接字类型已经包含在sock结构中。MY_PF_INET域中有效的类型是SOCK_STREAM, SOCK_DGRAM和SOCK_RAW。据此,我们定位到inetsw数组的某一项(一个链表的链表头),然后在这个链表中匹配protocol。

MY_PF_INET域中的常用的protocol是:IPPROTO_IP, IPPROTO_ICMP, IPPROTO_IGMP, IPPROTO_TCP, IPPROTO_UDP。其中IPPROTO_IP比较特殊,是一个通配符。链表中的protocol匹配可以是严格匹配,也可以是通配符匹配,但最终 protocol必须有一个确定的值,而不能是IPPROTO_IP。因为MY_PF_INET域中inetsw数组只有三项(SOCK_STREAM, IPPROTO_TCP), (SOCK_DGRAM, IPPROTO_UDP), (SOCK_RAW, IPPROTO_IP),所有这个匹配比较简单。

完成匹配后,我们要把匹配到的inet_protosw结构中的相关的值赋给sock。同时,创建sock的成员结构struct sock *sk,另外还涉及到一个结构体struct inet_sock的初始化,具体内容不在这里详述了。

最后,调用具体协议(TCP, UDP, RAW)的初始化函数,完成最后的创建工作。

到这里为止,我们的MY_PF_INET模块已经能够执行socket系统调用,并作出相应的动作,返回正确的值了。我们在三种协议的初始化函数中分别加入调试输出语句,并观察其行为(udp没有提供init函数)。

基于前面的积累,我们再来仔细分析socket这个系统调用:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

三个参数中,domain指定了一个套接字域,它会影响到内核具体选择使用哪一个模块。比如,在我们的测试程序中,可以使用MY_PF_INET(30) 使内核使用我们的my_inet.ko模块,建立因特网协议域。而type表示域中的套接字类型,对应inetsw数组的一项,在我们的 MY_PF_INET域中,其有效的取值是SOCK_STREAM, SOCK_DGRAM和SOCK_RAW。最后一个参数protocol,指定具体的协议类型,它可以使用通配符IPPROTO_IP匹配对应类型的链表中的第一项。下面的测试代码:

int fd0 = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_ICMP );
int fd1 = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_IGMP );
int fd2 = socket( MY_PF_INET, SOCK_RAW, IPPROTO_IP );
int fd3 = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
int fd4 = socket( MY_PF_INET, SOCK_DGRAM, IPPROTO_IP );
int fd5 = socket( MY_PF_INET, SOCK_STREAM, MY_IPPROTO_TCP );
int fd6 = socket( MY_PF_INET, SOCK_STREAM, IPPROTO_IP );
printf("fd: %d, %d, %d, %d, %d, %d, %d/n", fd0, fd1, fd2, fd3, fd4, fd5, fd6);

显然,SOCK_RAW是没有办法通配的。结果就很明显:

fd: 3, 4, -1, 5, 6, 7, 8

我们再来关注一下socket系统调用的一些主要出错情况。

第一种:所选的域不支持。domain在内核中的最大取值是31。共有32个值可选(包括四个空值,和一个保留值)。这个错误在__sock_create中就会被检测出。

第二种:套接字类型与协议类型不匹配。比如SOCK_DGRAM搭配IPPROTO_TCP,肯定会失败。

其余的出错类型,一般为系统错误,不必关注,实际编程中,只要判断返回的fd是否大于零即可。


销毁一个套接字


我们先来简单看一下系统调用close(int fd)的流程。该系统调用会调用到内核中的函数:


asmlinkage long sys_close(unsigned int fd)

参数fd给我们一个很好的线索,我们可以很方便地找到相应的struct file结构:file = current->files->fd[fd]。取出了这个至关重要的数据结构后,我们归还fd给系统,同时,设current-> file->fd[fd]=NULL。使我们创建的socket完全跟系统和进程分离。最后再销毁struct file结构。

销毁struct file的很多细节我们不关注,但在某一步,一个叫__fput(struct file *file)的函数中,有这样一个调用:

file->f_op->release(inode, file);

它实际调用到了sock_close函数,该函数又会调用到sock_release函数。sock_release函数又调用我们my_inet模块提供的myinet_release函数完成实际的socket销毁工作,同时,释放inode。

上面讲述的是一个大致的流程,我们重点关注的还是如何在我们的my_inet模块中实现套接字的销毁。下面先看一下myinet_release函数的实现:

int myinet_release(struct socket *sock) {
    struct sock *sk = sock->sk;

    if (sk) {
        long timeout = 0;
        myip_mc_drop_socket(sk);

        if (sock_flag(sk, SOCK_LINGER) && !(current->flags & PF_EXITING))
            timeout = sk->sk_lingertime;
        sock->sk = NULL;
        sk->sk_prot->close(sk, timeout);
    }
    return 0;
}
可见,在我们的模块中,我们关注的其实是sock->sk的销毁,它是网络层一个套接字表示。myip_mc_drop_socket是离开组播组(如果曾加入过组播组的话),我们目前不关注。关于timeout我们也暂时不关心。

这里调用到了我们对应的协议的close函数,暂时,我们不去关注很多细节,只要记得在这个函数中应该调用sk_common_release最后完成对sock->sk的资源释放即可。


套接字选项


套接字选项这个话题在socket编程里,可能已经属于中高级话题了,之所以在一开始就把这个话题提上来讲,是因为我们的一个近阶段目标是能够把 MY_PF_INET域的RAW协议走通,并在上面跑起一个ping程序,所以,按照ping程序的要求,接下来,我们必须实现套接字选项系统调用 setsockopt在MY_PF_INET中RAW协议中的相关实现。

下面是该系统调用函数的原型:

#include <sys/socket.h>
int setsockopt( int socket, int level, int option_name,
                    const void *option_value, size_t option_len);

第一个参数socket是套接字描述符。第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。option_name指定准备设置的选项,option_name可以有哪些取值,这取决于level,以linux 2.6内核为例(在不同的平台上,这种关系可能会有不同),在套接字级别上(SOL_SOCKET),option_name可以有以下取值:

  • SO_DEBUG,打开或关闭调试信息。 当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置SOCK_DBG(第10)位,或清SOCK_DBG位。
  • SO_REUSEADDR,打开或关闭地址复用功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。
  • SO_DONTROUTE,打开或关闭路由查找功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。
  • SO_BROADCAST,允许或禁止发送广播数据。当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。
  • SO_SNDBUF,设置发送缓冲区的大小。发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。该操作将sock->sk->sk_sndbuf设置为val * 2,之所以要乘以2,是防止大数据量的发送,突然导致缓冲区溢出。最后,该操作完成后,因为对发送缓冲的大小作了改变,要检查sleep队列,如果有进程正在等待写,将它们唤醒。
  • SO_RCVBUF,设置接收缓冲区的大小。接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)和256字节。该操作将sock->sk->sk_rcvbuf设置为val * 2。
  • SO_KEEPALIVE,套接字保活。如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时器,否则关闭保活定时器。对于所有协议,该操作都会根据option_value置或清sock->sk->sk_flag中的 SOCK_KEEPOPEN位。
  • SO_OOBINLINE,紧急数据放入普通数据流。该操作根据option_value的值置或清sock->sk->sk_flag中的SOCK_URGINLINE位。
  • SO_NO_CHECK,打开或关闭校验和。该操作根据option_value的值,设置sock->sk->sk_no_check。
  • SO_PRIORITY,设置在套接字发送的所有包的协议定义优先权。Linux通过这一值来排列网络队列。这个值在0到6之间(包括0和6),由option_value指定。赋给sock->sk->sk_priority。
  • SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。该选项的参数(option_value)是一个linger结构:
struct linger {
    int l_onoff;  /* 延时状态(打开/关闭) */
    int l_linger; /* 延时多长时间 */
};

如果linger.l_onoff值为0(关闭),则清sock->sk->sk_flag中的SOCK_LINGER位;否则,置该位,并赋sk->sk_lingertime值为linger.l_linger。

  • SO_PASSCRED,允许或禁止SCM_CREDENTIALS 控制消息的接收。该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_PASSCRED位。
  • SO_TIMESTAMP,打开或关闭数据报中的时间戳接收。该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_RCVTSTAMP位,如果打开,则还需设sock->sk->sk_flag中的SOCK_TIMESTAMP位,同时,将全局变量netstamp_needed加1。
  • SO_RCVLOWAT,设置接收数据前的缓冲区内的最小字节数。在Linux中,缓冲区内的最小字节数是固定的,为1。即将sock->sk->sk_rcvlowat固定赋值为1。
  • SO_RCVTIMEO,设置接收超时时间。该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。
  • SO_SNDTIMEO,设置发送超时时间。该选项最终将发送超时时间赋给sock->sk->sk_sndtimeo。
  • SO_BINDTODEVICE,将套接字绑定到一个特定的设备上。该选项最终将设备赋给sock->sk->sk_bound_dev_if。
  • SO_ATTACH_FILTER和SO_DETACH_FILTER。关于数据包过滤,它们最终会影响sk->sk_filter。
以上所介绍的都是在SOL_SOCKET层的一些套接字选项,如果超出这个范围,给出一些不在这一level的选项作为参数,最终会得到- ENOPROTOOPT的返回值。但以上的分析仅限于这些选项对sock-sk的值的影响,这些选项真正如何发挥作用,我们的探索道路将漫漫其修远。

如果不在套接字级别上设置选项,即setsockopt系统调用的参数level不设为SOL_SOCKET,那么sys_setsockopt的实现会直接调用sock->ops->setsockopt。对MY_PF_INET域的RAW协议来讲,sock->ops = myinet_sockraw_ops,而myinet_sockraw_ops.setsockopt = sock_common_setsockopt。

而sock_common_setsockopt直接调用sock->sk->sk_prot->setsockopt。对于RAW协议来讲,即myraw_setsockopt。

下面关注myraw_setsockopt的实现。对于RAW协议来讲,level还可以有两种取值:SOL_IP和SOL_RAW。 myraw_setsockopt首先检查level是否为SOL_IP,如果是,调用myip_setsockopt函数,该函数实现IP级别上的选项,否则,为SOL_RAW级别上的选项,SOL_RAW级别上只有一个选项,即ICMP_FILTER,在MY_IPPROTO_ICMP协议下有效。它激活绑定到MY_IPPROTO_ICMP协议的一个用于myraw socket特殊的过滤器。该值对每种ICMP消息都有一个位(掩码),可以把那种ICMP消息过滤掉,缺省时是不过滤ICMP消息。

对于ICMP_FILTER选项,myraw_setsockopt调用myraw_seticmpfilter函数,它把option_value赋给 sock->sk->filter,option_value是一个结构体:

struct icmp_filter {
     __u32       data;
};

它是一个32位的位掩码。

关于该位掩码,我们目前知道的是最低位为回显应答的位掩码,由于目前我们的MY_PF_INET域代码还没完善,我们在PF_INET域上进行测试,把下面的代码添加到一个ping程序中,ping程序就收不到来自服务器的回应包了:


#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>

#include <linux/in.h>
#include <linux/icmp.h>
int main()     
{
    struct icmp_filter filter;         
    socklen_t size = sizeof( struct icmp_filter );
    int fd = socket( PF_INET, SOCK_RAW, IPPROTO_ICMP );
    if( fd < 0 )
        perror("error: ");

    getsockopt( fd, SOL_RAW, ICMP_FILTER, &filter, &size );
    printf("the filter: %x/n", filter.data );

    filter.data = 1;
    int err = setsockopt( fd, SOL_RAW, ICMP_FILTER, &filter, sizeof(struct icmp_filter) );
    if( err < 0 )
        perror("error: ");

    memset( &filter, 0, sizeof( struct icmp_filter ) );
    getsockopt( fd, SOL_RAW, ICMP_FILTER, &filter, &size );
    printf("new filter: %x/n", filter.data);

    close(fd);
    return 0;
}

继续讲关于myraw_setsockopt的实现,如果level是SOL_IP,则调用myip_setsockopt函数。 myip_setsockopt的操作对像是struct socket sock的成员struct sock sk。并把sk强制转化为struct inet_sock: inet = inet_sk(sk)。
如果option_name在MRT_BASE和MRT_BASE+10之间,则调用myip_mroute_setsockopt函数,关于mroute,后面再给出分析。

  • IP_OPTIONS:设置将由该套接字发送的每个包的IP选项。
    其option_value是一个结构体struct ip_options。该选项首先分配一个这样的结构体,然后用这个结构体替代inet->opt指向的结构体。如果协议类型是 SOCK_STREAM的话,从struct tcp_sock *tp中,tp->ext_header_len减去旧的inet->opt->optlen, 再加上新的opt->optlen。最后调用tcp_sync_mss进行同步,有关TCP的一些细节,我们在实现TCP协议时再分析。
  • IP_PKTINFO:传递一条包含pktinfo结构(该结构提供一些来访包的相关信息)的IP_PKTINFO辅助信息。
    这个选项只对数据报类的套接字有效。
struct in_pktinfo
{
    unsigned int ipi_ifindex; /* 接口索引 */
    struct in_addr ipi_spec_dst; /* 路由目的地址 */
    struct in_addr ipi_addr; /* 头标识目的地址 */
};

ipi_ifindex指的是接收包的接口的唯一索引。ipi_spec_dst指的是路由表记录中的目的地址,而ipi_addr 指的是包头中的目的地址。如果给 sendmsg传递了IP_PKTINFO,那么外发的包会通过在ipi_ifindex中指定的接口发送出去,同时把ipi_spec_dst设置为目的地址。
myip_setsockopt的代码实现中只是根据option_value是否为0,置或清inet->cmsg_flags的IP_CMSG_PKTINFO位。

  • IP_RECVTTL:
    该选项根据option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_TTL位,具体用途,留待日后分析。
  • IP_RECVTOS:
    如果打开了这个选项,则IP_TOS辅助信息会与来访包一起传递。它包含一个字节用来指定包头中的服务/优先>级字段的类型。该字节为一个布尔整型标识。该选项根据option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_TOS位。
  • IP_RECVOPTS:
    用一条IP_OPTIONS控制信息传递所有来访的IP选项给用户。路由头标识和其它选项已经为本地主机填好.此选项不支持SOCK_STREAM套接字。该选项根据option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_RECVOPTS位。
  • IP_RETOPTS:
    等同于IP_RECVOPTS但是返回的是带有时间戳的未处理的原始选项和在这段路由中未填入的路由记录项目。该>选项根据 option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_RETOPTS位。
  • IP_TOS:
    设置源于该套接字的每个IP包的Type-Of-Service(TOS 服务类型)字段。它被用来在网络上区分包的优先级>。TOS是单字节的字段。定义了一些的标准TOS标识:IPTOS_LOWDELAY用来为交互式通信最小化延迟时间,IPTOS_THROUGHPUT用来优化吞吐量,IPTOS_RELIABILITY用来作可靠性优化, IPTOS_MINCOST应该被用作“填充数据”,对于这些数据,低速传输是无关紧要的。至多只能声明这些 TOS 值中的一个,其它的都是无效的,应当被清除。缺省时,Linux首先发送IPTOS_LOWDELAY数据报,但是确切的做法要看配置的排队规则而定。一些高优先级的层次可能会要求一个有效的用户标识0或者CAP_NET_ADMIN能力。优先级也可以以于协议无关的方式通过( SOL_SOCKET, SO_PRIORITY )套接字选项来设置。
    该选项的操作置inet->tos = val,sk->sk_priority = rt_tos2priority(val),同时,清sk->sk_dst_cache。
  • IP_TTL:设置从此套接字发出的包的当前生存时间字段。
    该选项置inet->uc_ttl = option_value。
  • IP_HDRINCL:
    该选项只对SOCK_RAW有效,如果提供的话,用户可在用户数据前面提供一个ip头。该选项的操作根据option_value是否为零,置inet->hdrincl为1或0。
  • IP_MTU_DISCOVER:
    为套接字设置Path MTU Discovery setting(路径MTU发现设置)。该选项的操作置inet->pmtudisc = option_value,option_value只允许取值0,1,2。
  • IP_SOL层上余下的选项还有:
    IP_RECVERR,IP_MULTICAST_TTL,IP_MULTICAST_LOOP,IP_MULTICAST_IF, IP_ADD_MEMBERSHIP,IP_DROP_MEMBERSHIP,IP_MSFILTER,IP_BLOCK_SOURCE, IP_UNBLOCK_SOURCE,IP_ADD_SOURCE_MEMBERSHIP,IP_DROP_SOURCE_MEMBERSHIP, MCAST_JOIN_GROUP,MCAST_LEAVE_GROUP,MCAST_JOIN_SOURCE_GROUP, MCAST_LEAVE_SOURCE_GROUP,MCAST_BLOCK_SOURCE,MCAST_UNBLOCK_SOURCE, MCAST_MSFILTER,IP_ROUTER_ALERT,IP_FREEBIND,IP_IPSEC_POLICY, IP_XFRM_POLICY。
    在涉及到相关内容时,再进行一一分析。


向套接字写数据


套接字写有多个实现接口,我们只以其中一个接口write为线索,对套接字写(网络数据发送)的流程进行分析。系统调用write会调用内核函数sys_write,sys_write调用vfs_write完成实际的写操作。

vfs_write会先调用file->f_op->write(file从套接字描述符获得)。如果file->f_op-> write不存在,则调用do_sync_write。该函数会调用sock_aio_write,sock_aio_write又会调用 __sock_sendmsg,然后到myinet_sendmsg,最后才到sk->sk_prot->sendmsg,对于RAW协议来讲,即myraw_sendmsg。

sock_aio_write的函数原型如下:

static ssize_t sock_aio_write(struct kiocb *iocb, const char __user *ubuf,
              size_t size, loff_t pos)

ubuf是用户待发送数据,size是数据长度,pos是文件位置(永远为零)。在这个函数里,会把用户待发送数据封装成一个struct msghdr结构:

struct msghdr {
    void *          msg_name;    /* Socket name          */
    int             msg_namelen; /* Length of name       */
    struct iovec *  msg_iov;     /* Data blocks          */
    __kernel_size_t msg_iovlen;  /* Number of blocks     */
    void *msg_control; /* Per protocol magic (eg BSD file descriptor passing) */
    __kernel_size_t msg_controllen; /* Length of cmsg list */
    unsigned        msg_flags;
};

如果用户代码为: write(fd, "abcdef", 6 ),则在sock_aio_write中封装成的msghdr结构为:

struct msghdr thehdr{
    .msg_name = NULL,
    .msg_namelen = 0,
    .msg_iov.iov_base = "abcdef",
    .msg_iov.iov_len = 6,
    .msg_iovlen = 1,
    .msg_control = NULL,
    .msg_controllen = 0,
    .msg_flags = 0
};

myraw_sendmsg的函数原型为:

static int myraw_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                            size_t len)

所以,它拿到的是已经封装好的消息msg。该函数所做的第一件事情是检查len,其最大长度是16位(0xffff),然后,确认msg->msg_flags中没有MSG_OOB(RAW不支持带外数据的发送)。

如果msg->msg_namelen不等于零,则name中存储的是域和目的地址的信息,如果等于零,则当前必须是已经建立了TCP连接的,否则数据不知道发往哪儿。

接下来,查看控制数据缓冲区长度是否为零,如果不是,则有控制信息msg->msg_control,调用ip_cmsg_send发送控制信息(实际上,主要是填充一个结构体struct ipcm_cookie ipc,从代码来看,该结构应该用于构建ip头)。

inet->hdrincl表示需要自己来构建ip头,所以如果inet->hdrincl==1,并且,ipc->opt!=NULL则返回出错信息:无效参数。

接下来判断目的地址是否为组播地址(组播地址的最高四位为1110),是则作相应处理。

接下来,声明并初始化一个struct flowi结构,如果不是自己构建ip头,则调用raw_probe_proto_opt

接下来的内容,暂时未能很好理解,留待下文分析。


操作网络设备的几个命令


一般,用户在shell中使用ifconfig命令对网络接口进行参数配置,及接口的打开,关闭等操作。ifconfig实现网络接口配置的原理在于代表网络接口的结构体struct net_device的成员ip_ptr。前文已经讲过,ip_ptr实际指向的是一个结构体struct in_devicein_device有一个成员struct in_ifaddr *ifa_list,它指向一个链表,链表的每一项代表一个IP地址。对这个链表操作即可实现对网络接口的配置。
网络接口的操作命令按功能可以分为两组,第一组为查询命令:SIOCGIFADDR,SIOCGIFBRDADDR,SIOCGIFDSTADDR, SIOCGIFNETMASK。分别用于查询网络接口的IP地址,广播地址,目的地址,子网掩码。第二组为设置命令:SIOCSIFADDR, SIOCSIFFLAGS,SIOCSIFBRDADDR,SIOCSIFNETMASK,SIOCSIFDSTADDR。分别用于设置网络接口的IP地址,标志位,广播地址,子网掩码,目的地址。这些命令所要查询和设置的信息全部在结构体struct in_ifaddr中。
用户空间的应用程序通过系统调用ioctl使用这些命令,ioctl的函数原型如下:

#include <sys/ioctl.h>
int ioctl(int d, int request, ...);

以上九个命令使用的参数为同一类型,即struct ifreq,其定义可在include/linux/if.h中找到。

下面是两组命令在my_inet模块中的使用示例,因为整个my_inet模块代码还很不完善,通过my_inet模块新添加的IP地址并不能正常使用。

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/if.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>
#include <string.h>
 
int main()
{
    struct ifreq req;
    strncpy( req.ifr_name, "eth0", IFNAMSIZ );
    struct sockaddr_in *sin;
 
    int fd = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_ICMP );
    if( fd < 0 ){
        perror("error: ");
        return -1;
    }
 
    sin = (struct sockaddr_in *)&req.ifr_addr;
    if( ioctl( fd, SIOCGIFADDR, &req) == 0 )
        printf("%s/n", inet_ntoa(sin->sin_addr.s_addr) );
    else
        perror("ioctl error: ");
 
    sin = (struct sockaddr_in *)&req.ifr_broadaddr;
    if( ioctl( fd, SIOCGIFBRDADDR, &req) == 0 )
        printf("%s/n", inet_ntoa(sin->sin_addr.s_addr) );
    else
        perror("ioctl error: ");
 
    sin = (struct sockaddr_in *)&req.ifr_dstaddr;
    if( ioctl( fd, SIOCGIFDSTADDR, &req) == 0 )
        printf("%s/n", inet_ntoa(sin->sin_addr.s_addr) );
    else
        perror("ioctl error: ");
 
    sin = (struct sockaddr_in *)&req.ifr_netmask;
    if( ioctl( fd, SIOCGIFNETMASK, &req) == 0 )
        printf("%s/n", inet_ntoa(sin->sin_addr.s_addr) );
    else
        perror("ioctl error: ");
 
    close( fd );
    return 0;
}
 
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/if.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>
#include <string.h>
 
int main()
{
    struct ifreq req;
    strncpy( req.ifr_name, "eth0:0", IFNAMSIZ );
    struct sockaddr_in *sin;
 
    int fd = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_ICMP );
    if( fd < 0 ){
        perror("error: ");
        return -1;
    }
 
    sin = (struct sockaddr_in *)&req.ifr_addr;
    sin->sin_family = MY_PF_INET;
    if( inet_aton("172.16.48.10", &sin->sin_addr) == 0 ){
        perror("inet_aton error: ");
        return -1;
    }
    if( ioctl( fd, SIOCSIFADDR, &req ) == 0 ){
        printf("success!/n");
    }else{
        perror("ioctl failed: ");
    }
 
    req.ifr_flags = IFF_UP;
    if( ioctl( fd, SIOCSIFFLAGS, &req ) == 0 ){
        printf("success!/n");
    }else{
        perror("ioctl failed: ");
    }
 
    sin->sin_family = MY_PF_INET;
    if( inet_aton("172.16.48.255", &sin->sin_addr) == 0 ){
        perror("inet_aton error: ");
        return -1;
    }
    if( ioctl( fd, SIOCSIFBRDADDR, &req ) == 0 ){
        printf("success!/n");
    }else{
        perror("ioctl failed: ");
    }
 
    sin->sin_family = MY_PF_INET;
    if( inet_aton("255.255.255.0", &sin->sin_addr) == 0 ){
        perror("inet_aton error: ");
        return -1;
    }
    if( ioctl( fd, SIOCSIFNETMASK, &req ) == 0 ){
        printf("success!/n");
    }else{
        perror("ioctl failed: ");
    }
 
    close( fd );
    return 0;
}

要想让上述的代码发挥实际的作用,只要把所有的MY_PF_INET改成PF_INET即可。功能再作增强,就是一个ifconfig程序了。


加入对raw socket(原始套接字)类型的支持


变量inetsw_array是inet域的一个全局数组,其类型是struct inet_protosw,该结构体的定义如下


struct inet_protosw {
    struct list_head    list;
    unsigned short      type;
    int                 protocol;
    struct proto        *prot;
    const struct proto_ops *ops;
    int              capability;
    char             no_check;
    unsigned char    flags;
};
type是指套接字的类型,也就是系统调用socket的第二个参数,inet域支持的套接字类型有SOCK_STREAM(流套接字),它是一个有序的,可靠的,基于连接的双向字节流;SOCK_DGRAM(数据报套接字),该类型的套接字是不可靠的,无连接的,有可能乱序的;SOCK_RAW(原始套接字),所谓原始,是因为该类型的套接字不提供传输层的服务,协议栈只为该类型的套接字自动添加网络层首部,常用于网络层的附属协议(icmp, igmp等)。 protocol是一个传输层协议号,传输层的协议包括IPPROTO_TCP,IPPROTO_UDP;或者对于SOCK_RAW来讲,它是一个通配协议号IPPROTO_IP,用于通配网络层的附属协议icmp,igmp等。对于传输层协议来讲,IPPROTO_TCP对应的套接字类型总是SOCK_STREAM,IPPRTO_UDP对应的套接字类型总是STREAM_DGRAM,所以在socket系统调用时,可以不必指定协议号,而直接使用通配符IPPROTO_IP。 prot是一个传输层协议绑定的操作集,比如对于IPPROTO_TCP,它就是tcp_prot,对于IPPROTO_UDP,它就是udp_prot。而对于类型为SOCK_RAW的套接字,它没有相应的传输层协议,而是用于通配所有的网络层附属协议,所以,prot就是所有网络层附属协议共用的一个操作集raw_prot。 ops是套接字类型绑定的操作集,对应于SOCK_STREAM, SOCK_DGRAM, SOCK_RAW,操作集分别为inet_stream_ops,inet_dgram_ops,inet_sockraw_ops。 capability是操作这类别套接字所需要的权限,除了原始套接字需要CAP_NET_RAW权限之外,其它两类套接字不需要特殊权限(-1)。 flags的可能取值如下:
#define INET_PROTOSW_REUSE 0x01      /* Are ports automatically reusable? */
#define INET_PROTOSW_PERMANENT 0x02  /* Permanent protocols are unremovable. */
#define INET_PROTOSW_ICSK      0x04  /* Is this an inet_connection_sock? */

inetsw是一个链表数组,每一项都是一个struct inet_protosw结构体的链表,总共有SOCK_MAX项,在inet_init函数对INET域进行初始化的时候,调用函数inet_register_protosw把数组inetsw_array中定义的套接字类型全部注册到inetsw数组中,相同套接字类型,不同协议类型的在数组的同一项,以套接字类型为索引,在系统实际使用的时候,只使用inetsw,而不使用inetsw_array,目前inet域不存在相同套接字类型的多个协议(原始套接字使用通配符,所以也不存在这个问题)。

使用系统调用socket创建一个RAW类型的套接字,并且网络层附属协议为icmp的时候,首先会在函数__sock_create中创建一个传输层的struct socket,如下:

struct socket {
        socket_state            state;
        unsigned long           flags;
        const struct proto_ops  *ops;
        struct fasync_struct    *fasync_list;
        struct file             *file;
        struct sock             *sk;
        wait_queue_head_t       wait;
        short                   type = SOCK_RAW;
    };

INET域就在net_families数组的第三项(下标为2),其结构体定义如下:

struct net_proto_family{
    int         family;
    int         (*create)(struct socket *sock, int protocol);
    short       authentication;
    short       encryption;
    short       encrypt_net;
    struct module   *owner;
};

通过函数sock_register注册到数组中,因为socket系统调用的第一个参数指定了协议域号,所以__sock_create可以通过net_families[2]->create调用到INET域的创建函数,完成进一步的socket创建工作。
inet_create函数通过套接字类型socket->type找到inetsw[SOCK_RAW],并从该链表(其实只有一项)的头部开始匹配网络层附属协议号,因为SOCK_RAW类型的套接字采用通配符,所以匹配成功。同时,还需要检查权限,inet_create函数进一步完善了struct socket的内容:

struct socket {
    socket_state            state = SS_UNCONNECTED;
    unsigned long           flags = INET_PROTOSW_REUSE;
    const struct proto_ops  *ops = &inet_sockraw_ops;
    struct fasync_struct    *fasync_list;
    struct file             *file;
    struct sock             *sk;
    wait_queue_head_t       wait;
    short                   type = SOCK_RAW;
};

struct socket是传输层的套接字,它只看到套接字类型这一层次,所以其操作函数是inet_sockraw_ops。inet_create还为其成员sk创建了一个网络层的套接字,其内容如下(只列出本文内容相关的部分):

struct sock{
    .sk_family = AF_INET;
    .sk_prot = &raw_prot;
    .sk_prot_creator = &raw_prot;
    .sk_reuse = 1;
    .sk_protocol = IPPROTO_ICMP;
    ... ...
}

这样,在一个套接字上作一个操作,比如说send,会先调用struct socket->ops->sendmsg(),该函数会调用struct socket->sk->sk_prot->sendmsg完成实际的发送工作。

综上所述,现在要加入对原始套接(SOCK_RAW)的支持,首先要在数组inetsw_array中提供一个原始套接字类型的struct inet_protosw,在INET域初始化的时候会被加入到inetsw数组中,同时提供一个原始套接字上的所有网络层附属协议的通用操作集raw_prot和原始套接字类型的操作集inet_sockraw_ops,并实现它们的全部函数即可。中间涉及到具体的协议,还会有一些协议相关的支持问题要处理。


回顾


首先以socket和send两个系统调用为例,来回顾一下协议栈是如何工作的,在这过程中可以找到如何在协议栈中增加对UDP协议的支持。socket系统调用的原型是

int socket(int domain, int type, int protocol);

domain是协议域,对于ipv4协议来说,其值是PF_INET(ipv4因特网协议),对于我们自己实现的ipv4协议模块,我们为其新增MY_PF_INET。所有的协议域在 include/linux/socket.h被定义,如下:

#define AF_UNSPEC 0
#define AF_UNIX  1  // Unix域的socket
#define AF_LOCAL 1  // AF_UNIX的POSIX命名
#define AF_INET  2  // 因特网IP协议
#define AF_AX25  3  // Amateur Radio AX.25
#define AF_IPX  4  // Novell IPX
#define AF_APPLETALK 5 // AppleTalk DDP
#define AF_NETROM 6   // Amateur Radio NET/ROM
#define AF_BRIDGE 7   // Multiprotocol bridge
#define AF_ATMPVC 8   // ATM PVCs
#define AF_X25  9   // Reserved for X.25 project
#define AF_INET6 10  // IP version 6
#define AF_ROSE  11  // Amateur Radio X.25 PLP
#define AF_DECnet 12  // Reserved for DECnet project
#define AF_NETBEUI 13 // Reserved for 802.2LLC project
#define AF_SECURITY 14 // Security callback pseudo AF
#define AF_KEY  15  // PF_KEY key management API
#define AF_NETLINK 16
#define AF_ROUTE AF_NETLINK // Alias to emulate 4.4BSD
#define AF_PACKET 17     // Packet family
#define AF_ASH  18     // Ash
#define AF_ECONET 19     // Acorn Econet
#define AF_ATMSVC 20     // ATM SVCs
#define AF_SNA  22     // Linux SNA Project (nutters!)
#define AF_IRDA  23     // IRDA sockets
#define AF_PPPOX 24     // PPPoX sockets
#define AF_WANPIPE 25    // Wanpipe API Sockets
#define AF_LLC  26      // Linux LLC
#define AF_BLUETOOTH 31   // Bluetooth sockets
#define AF_MAX  32     // For now..

可以看到,当前,内核最多支持31个协议域(0为未指定,32为MAX)。而当前的定义中还有27,28,30为空,所以我们定义了MY_PF_INET为28。

在内核中,结构体struct net_proto_family用于表示一个协议域,而全局数组变量static struct net_proto_family *net_families[NPROTO]是一个有32项的数组,用于保存当前内核中所有已注册的协议域,函数sock_register用于把一个协议域注册到内核中,即把一个协议域跟net_families数组

中的某一项相关联。struct net_proto_family的完整定义如下:

struct net_proto_family {
 int  family;
 int  (*create)(struct socket *sock, int protocol);
 short  authentication;
 short  encryption;
 short  encrypt_net;
 struct module *owner;
};

其中,family为域编号,对于我们的模块即为MY_PF_INET。通过sock_register函数,使net_families[MY_PF_INET]指向需要注册的域。create是该域的socket的创建函数,我们的MY_PF_INET域定义如下:

static struct net_proto_family myinet_family_ops = {
    .family = MY_PF_INET,
    .create = myinet_create,
    .owner  = THIS_MODULE,
};

现在回到socket系统调用上来,内核实现socket系统调用的函数是sys_socket。该函数通过调用sock_create进行创建,sock_create调用__sock_create。__sock_create要创建一个struct socket,这是一个普通BSD socket的结构体,其定义如下:

struct socket {
    socket_state  state;
    unsigned long  flags;
    struct proto_ops *ops;
    struct fasync_struct *fasync_list;
    struct file  *file;
    struct sock  *sk;
    wait_queue_head_t wait;
    short   type;
    
};

__sock_create创建的时候,为其type赋上socket系统调用的第二个参数type,最后通过调用net_families[family]->create(sock, protocol)完成socket的创建。对于MY_PF_INET域来说,该create函数即myinet_create。MY_PF_INET域支持的网络层协议是IP协议,在该协议上支持的套接字接口有流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)和原始套接字(SOCK_RAW)。在IP协议上注册一个套接字接口,也即创建一个套接字,需要知道该类型的套接字必需的一些相关信息。结构体struct inet_protosw就是用于在IP协议上注册套接字接口,其完整定义如下:

struct inet_protosw {
    struct list_head list;
    unsigned short  type;    //套接字类型,即socket系统调用的第二个参数。
    int   protocol;       //第4层(传输层)协议号
    struct proto  *prot;    //第4层协议的操作函数集
    struct proto_ops *ops;   //该类型的套接字的操作函数集
    int capability;
    char no_check;
    unsigned char  flags;
};

myinet_create函数注册套接字的过程本质上就是为指定套接字类型和第4层协议号的一个socket找到对应的操作函数集,使这个socket随后能真正被操作。全局数组inetsw_array包含了系统当前支持的所有在IP协议上能够注册的套接字接口,在系统初始化的时候,这些结构体以type作为依据,被组织到

static struct list_head inetsw[SOCK_MAX]中。当在inetsw数组中找到对应的socket类型和第4层协议号后,令struct socket->ops的值为struct inet_protosw->ops,即为该类型的套接字指定操作函数集。而struct socket->sk是网络层的套接字接口,其成员sk_prot的值为struct inet_protosw->prot,即为该类型的第4层协议指定操作函数集。套接字的创建工作大致如此。

接下来,再来看send系统调用,它的原型如下:

ssize_t send(int s, const void *buf, size_t len, int flags);

s是文件描述符,在内核中跟一个struct socket结构体建立一一对应的映射关系。buf和len分别为待发送数据的内容和长度,flag是一些标志位。内核实现该系统调用的函数是sys_send。sys_send直接调用sys_sendto,把sys_sendto的最后两个参数addr和addr_len置空。sys_sendto根据文件描述符s找到对应的struct socket,然后建立一个结构体struct msghdr msg用于发送数据内容,该结构体的定义如下:

struct msghdr {
    void *          msg_name;    /* Socket 的名字 */
    int             msg_namelen; /* 名字的长度  */
    struct iovec *  msg_iov;     /* 数据块   */
    __kernel_size_t msg_iovlen;  /* 数据块的数量  */
    void *msg_control; /* Per protocol magic (eg BSD file descriptor passing) */
    __kernel_size_t msg_controllen; /* Length of cmsg list */
    unsigned        msg_flags;
};

然后,sys_sendto调用sock_sendmsg发送数据,sock_sendmsg调用__sock_sendmsg__sock_sendmsg调用struct socket->ops->sendmsg,即调用特定套接字类型的操作函数集中的sendmsg成员函数。比如,SOCK_RAW类型的套接字的sendmsg成员函数的实现如下(实际上SOCK_DGRAM类型的套接字的sendmsg成员函数也是这个):

可以看到,在该函数中,调用了具体的第4层协议的操作函数集中的sendmsg成员函数,而该函数真正实现了对应协议的数据报文发送工作。

原文地址:https://www.cnblogs.com/Kimbing-Ng/p/12493432.html