------------------------------------------------------------------------------------------------------------------------------
在未连接的Socket上发送UDP数据报
http://hi.baidu.com/linux%5Fkernel/blog/item/c12964386a10902396ddd814.html
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数据报
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/c7166d8128d418d8bd3e1ee3.html
很多介绍网络编程的书籍中会这样介绍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通讯的两端
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/a0b1962b0f7331fae7cd4079.html
说起套接字的通讯,我们一般会想到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数据报
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/c12964385ae9802397ddd883.html
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本地端口
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/a0cd024f888bf337aec3ab42.html
当建立一个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系统调用
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/ba9c32fad482fe1fa8d311d7.html
下面是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数据报
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/dfae34fa638927889e51468a.html
在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。
-----------------------------------------------------------------------------------------------------------------------------------
从套接字上得到扩展的更为可靠的出错信息
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/312ee8506e76c45c1138c261.html
在前一篇中,我们提到在对端主机上没有创建指定的UDP套接字时,我们向其发送一个UDP包,会得到一个目的端口不可达的ICMP出错报文。但内核在处理完该报文后,给应用程序仅仅返回一个ECONNREFUSED错误号,所以应用程序能知道的全部信息就是连接被拒绝,至于为什么被拒绝,没有办法知道。我们可以通过套接字选项的设置,让内核返回更为详细的出错信息,以利于调试程序,发现问题。下面是通过套接字选项传递扩展出错信息的一个示例程序。关于内核原理的分析,在下一篇给出。
#include <sys/socket.h> #include <linux/types.h> #include <linux/errqueue.h> #include <sys/ioctl.h> #include "my_inet.h" #include <stdio.h> #include <errno.h> #include <string.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!
----------------------------------------------------------------------------------------------------------------------------------
从套接字上得到扩展的更为可靠的出错信息(续)
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/58eafaf2d2b49113b17ec52a.html
接着前一篇,我们来看这个应用程序背后,内核真正做了一些什么事情。
代表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)
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/41442f738862bf1c8601b03b.html
用户使用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)
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/5c8510dfb2348613622798e5.html
接着上一篇,继续介绍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
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/3dd3dbb4b6d43f738bd4b287.html
一个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;
#define SOCK_ASYNC_NOSPACE 0 #define SOCK_ASYNC_WAITDATA 1 #define SOCK_NOSPACE 2 #define SOCK_PASSCRED 3
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详解
转自 http://hi.baidu.com/linux%5Fkernel/blog/item/017863d9e1e9462c11df9b87.html
结构体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在套接口释