socket的创建过程源码分析
1) socket, sock, inet_sock, tcp_sock的关系
创建完sk变量后,回到inet_create函数中:
这里是根据sk变量得到inet_sock变量的地址;这里注意区分各个不同结构体。
a. struct socket:这个是基本的BSD socket,面向用户空间,应用程序通过系统调用开始创建的socket都是该结构体,它是基于虚拟文件系统创建出来的;
类型主要有三种,即流式、数据报、原始套接字协议;
b. struct sock:它是网络层的socket;对应有TCP、UDP、RAW三种,面向内核驱动;
其状态相比socket结构更精细:
c. struct inet_sock:它是INET域的socket表示,是对struct sock的一个扩展,提供INET域的一些属性,如TTL,组播列表,IP地址,端口等;
d. struct raw_socket:它是RAW协议的一个socket表示,是对struct inet_sock的扩展,它要处理与ICMP相关的内容;
e. sturct udp_sock:它是UDP协议的socket表示,是对struct inet_sock的扩展;
f. struct inet_connection_sock:它是所有面向连接的socket表示,是对struct inet_sock的扩展;
g. struct tcp_sock:它是TCP协议的socket表示,是对struct inet_connection_sock的扩展,主要增加滑动窗口,拥塞控制一些TCP专用属性;
h. struct inet_timewait_sock:它是网络层用于超时控制的socket表示;
i. struct tcp_timewait_sock:它是TCP协议用于超时控制的socket表示;
三:具体过程
1、函数入口:
1) 示例代码如下:
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
2) 入口:
net/Socket.c:sys_socketcall(),根据子系统调用号,创建socket会执行sys_socket()函数;
2、分配socket结构:
1) 调用链:
net/Socket.c:sys_socket()->sock_create()->__sock_create()->sock_alloc();
2) 在socket文件系统中创建i节点:
inode = new_inode(sock_mnt->mnt_sb);
这里,new_inode函数是文件系统的通用函数,其作用是在相应的文件系统中创建一个inode;其主要代码如下(fs/Inode.c):
上面有个条件判断:if (sb->s_op->alloc_inode),意思是说如果当前文件系统的超级块有自己分配inode的操作函数,则调用它自己的函数分配inode,否则从公用的高速缓存区中分配一块inode;
3) 创建socket专用inode:
在“socket文件系统注册”一文中后面提到,在安装socket文件系统时,会初始化该文件系统的超级块,此时会初始化超级块的操作指针s_op为sockfs_ops结构;因此此时分配inode会调用sock_alloc_inode函数来完成:实际上分配了一个socket_alloc结构体,该结构体包含socket和inode,但最终返回的是该结构体中的inode成员;至此,socket结构和inode结构均分配完毕;分配inode后,应用程序便可以通过文件描述符对socket进行read()/write()之类的操作,这个是由虚拟文件系统(VFS)来完成的。
3、根据inode取得socket对象:
由于创建inode是文件系统的通用逻辑,因此其返回值是inode对象的指针;但这里在创建socket的inode后,需要根据inode得到socket对象;内联函数SOCKET_I由此而来,这里使用两个重要宏containerof和offsetof
4、使用协议族来初始化socket:
1) 注册AF_INET协议域:
在“socket文件系统注册”中提到系统初始化的工作,AF_INET的注册也正是通过这个来完成的;
初始化入口net/ipv4/Af_inet.c:这里调用sock_register函数来完成注册:
根据family将AF_INET协议域inet_family_ops注册到内核中的net_families数组中;下面是其定义:
static struct net_proto_family inet_family_ops = { .family = PF_INET, .create = inet_create, .owner = THIS_MODULE, };
其中,family指定协议域的类型,create指向相应协议域的socket的创建函数;
2) 套接字类型
在相同的协议域下,可能会存在多个套接字类型;如AF_INET域下存在流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW),在这三种类型的套接字上建立的协议分别是TCP, UDP,ICMP/IGMP等。
在Linux内核中,结构体struct proto表示域中的一个套接字类型,它提供该类型套接字上的所有操作及相关数据(在内核初始化时会分配相应的高速缓冲区,见上面提到的inet_init函数)。
AF_IENT域的这三种套接字类型定义用结构体inet_protosw(net/ipv4/Af_inet.c)来表示,如下:其中,tcp_prot(net/ipv4/Tcp_ipv4.c)、 udp_prot(net/ipv4/Udp.c)、raw_prot(net/ipv4/Raw.c)分别表示三种类型的套接字,分别表示相应套接字的 操作和相关数据;ops成员提供该协议域的全部操作集合,针对三种不同的套接字类型,有三种不同的域操作inet_stream_ops、 inet_dgram_ops、inet_sockraw_ops,其定义均位于net/ipv4/Af_inet.c下;
内 核初始化时,在inet_init中,会将不同的套接字存放到全局变量inetsw中统一管理;inetsw是一个链表数组,每一项都是一个struct inet_protosw结构体的链表,总共有SOCK_MAX项,在inet_init函数对AF_INET域进行初始化的时候,调用函数 inet_register_protosw把数组inetsw_array中定义的套接字类型全部注册到inetsw数组中;其中相同套接字类型,不同 协议类型的套接字通过链表存放在到inetsw数组中,以套接字类型为索引,在系统实际使用的时候,只使用inetsw,而不使用 inetsw_array;
3) 使用协议域来初始化socket
了解了上面的知识后,我们再回到net/Socket.c:sys_socket()->sock_create()->__sock_create()中:
pf = rcu_dereference(net_families[family]); err = pf->create(net, sock, protocol);
上面的代码中,找到内核初始化时注册的协议域,然后调用其create方法;
5、分配sock结构:
sk是网络层对于socket的表示,结构体struct sock比较庞大,这里不详细列出,只介绍一些重要的成员,sk_prot和sk_prot_creator,这两个成员指向特定的协议处理函数集,其类型是结构体struct proto,struct proto类型的变量在协议栈中总共也有三个.其调用链如下:
net/Socket.c:sys_socket()->sock_create()->__sock_create()->net/ipv4/Af_inet.c:inet_create();
inet_create()主要完成以下几个工作:
1) 设置socket的状态为SS_UNCONNECTED;
sock->state = SS_UNCONNECTED;
2) 根据socket的type找到对应的套接字类型:
由于同一type不同protocol的套接字保存在inetsw中的同一链表中,因此需要遍历链表来查找;在上面的例子中,会将protocol重新赋值为answer->protocol,即IPPROTO_TCP,其值为6;
3) 使用匹配的协议族操作集初始化sk;
结合源码,sock变量的ops指向inet_stream_ops结构体变量;
4) 分配sock结构体变量 net/Socket.c:sys_socket()->sock_create()->__sock_create()->net /ipv4/Af_inet.c:inet_create()->net/core/Sock.c:sk_alloc():
其中,answer_prot指向tcp_prot结构体变量;
其中,sk_prot_alloc分配sock结构体变量;由于在inet_init中为不同的套接字分配了高速缓冲区,因此该sock结构体变量会在该缓冲区中分配空间;分配完成后,对其做一些初始化工作:
i) 初始化sk变量的sk_prot和sk_prot_creator;
ii) 初始化sk变量的等待队列;
iii) 设置net空间结构,并增加引用计数;
6、建立socket结构与sock结构的关系:
inet = inet_sk(sk);
这里为什么能直接将sock结构体变量强制转化为inet_sock结构体变量呢?只有一种可能,那就是在分配sock结构体变量时,真正分配的是inet_sock或是其他结构体;
我们回到分配sock结构体的那块代码(参考前面的5.4小节:net/core/Sock.c):
static struct sock *sk_prot_alloc(struct proto *prot, gfp_t priority, int family) { struct sock *sk; struct kmem_cache *slab; slab = prot->slab; if (slab != NULL) sk = kmem_cache_alloc(slab, priority); else sk = kmalloc(prot->obj_size, priority); return sk; }
上面的代码在分配sock结构体时,有两种途径,一是从tcp专用高速缓存中分配;二是从内存直接分配;前者在初始化高速缓存时,指定了结构体大小为prot->obj_size;后者也有指定大小为prot->obj_size,
根据这点,我们看下tcp_prot变量中的obj_size(net/ipv4/Tcp_ipv4.c):
.obj_size = sizeof(struct tcp_sock),
也就是说,分配的真实结构体是tcp_sock;由于tcp_sock、inet_connection_sock、inet_sock、sock之间均为0处偏移量,因此可以直接将tcp_sock直接强制转化为inet_sock。
2) 建立socket, sock的关系
创建完sock变量之后,便是初始化sock结构体,并建立sock与socket之间的引用关系;调用链如下:
net/Socket.c:sys_socket()->sock_create()->__sock_create()->net /ipv4/Af_inet.c:inet_create()->net/core/Sock.c:sock_init_data():
该函数主要工作是:
a. 初始化sock结构的缓冲区、队列等;
b. 初始化sock结构的状态为TCP_CLOSE;
c. 建立socket与sock结构的相互引用关系;
7、使用tcp协议初始化sock:
inet_create()函数最后,通过相应的协议来初始化sock结构:这里调用的是tcp_prot的init钩子函数net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock(),它主要是对tcp_sock和inet_connection_sock进行一些初始化;
8、socket与文件系统关联:
创建好与socket相关的结构后,需要与文件系统关联,详见sock_map_fd()函数:
1) 申请文件描述符,并分配file结构和目录项结构;
2) 关联socket相关的文件操作函数表和目录项操作函数表;
3) 将file->private_date指向socket;
socket与文件系统关联后,以后便可以通过文件系统read/write对socket进行操作了;
1、socket():
函数原型:int socket(int domain,int type, int protocol);
函数作用:用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数
参数:domain:协议族/域,通常AF_INET(IPv4)、AF_INET6(IPv6)
type:是套接口类型,主要SOCK_STREAM(TCP协议)、SOCK_DGRAM(UDP协议)
protocol:一般为0
返回:成功时返回非负整数。
2、(1)、htons():
函数原型:uint16_t htons(uint16_t hostshort);
函数作用:htons是将整型变量从主机字节顺序转变成网络字节顺序,
就是整数在地址空间存储方式变为高位字节存放在内存的低地址处,
网络字节顺序采用big-endian排序方式。
htonl():
函数原型:uint32_t htonl(uint32_t hostlong);
函数作用:本函数将一个32位数从主机字节顺序转换成网络字节顺序。
(2)、inet_ntoa():
函数原型:char *inet_ntoa(struct in_addr in);
函数作用:将IP地址,转换为点分十进制的字符串格式;
注意为大端模式即0x78对应120,0x56对应86.......
(3)、inet_pton()
原型: int inet_pton(int af, const char *src, void *dst);
函数作用:将点分十进制的字符串格式的IP地址,转换成整数格式。
参数:af : AF_INET代表IPv4; AF_INET6 代表IPv6;
src:字符串格式点分十进制的IP地址;
dst:转换后的IP地址。
3、inet_addr():
原型:in_addr_t inet_addr(const char *cp);
函数作用:inet_addr()的功能是将一个点分十进制的IP转换成一个长整数型数(u_long类型)。
参数:字符串,一个点分十进制的IP地址。
4、select 函数以及 select相关函数:
函数原型: int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
函数作用:它能够监视我们需要监视的文件描述符的变化情况——读写或是异常
两个结构体:struct fd_set 可以理解为一个集合,这个集合中存放的是文件描述符,
fd_set集合可以通过一些宏由人为来操作,比如清空集合:FD_ZERO(fd_set*),
将一个给定的文件描述符加入集合之中 FD_SET(int, fd_set*),
将一个给定的文件描述符从集合中删除 FD_CLR(int, fd_set*),
FD_ISSET(int, fd_set*):检查集合中指定的文件描述符是否可以读写 ,
即检查 int所对应的套接字是否在这个集合当中,
select函数后将更新这个集合,把其中不可读的套节字去掉 。
struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,
一个是秒数,另一个毫秒数,即select函数是非阻塞的,超过这个设定的时间后可退出。
这个时间设置的用法:
第一:若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,
一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二:若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,
都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,
超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
参数解释:
readfds: (可选)指针,指向一组等待可读性检查的套接口;
writefds: (可选)指针,指向一组等待可写性检查的套接口;
exceptfds:(可选)指针,指向一组等待错误检查的套接口;
timeout: 本函数最多等待时间,对阻塞操作则为NULL。
5、bind函数:
函数原型:bind( SOCKET sockaddr, const struct sockaddr my_addr,int addrlen);
函数作用:套接字绑定到一个地址,并制定一个端口号。
将套接字绑定一个IP地址和端口号,因为这两个元素可以在网络环境中唯一地址表示一个进程。
6、listen函数:
函数原型:int listen(SOCKET sockfd, int backlog);
函数作用:listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,
从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
listen函数一般在调用bind之后-调用accept之前调用。
7、accept函数:
函数原型: int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen)。
函数作用:在服务器端,socket()返回的套接字用于监听(listen)和接受(accept)客户端的连接请求。
这个套接字不能用于与客户端之间发送和接收数据。
accept()接受一个客户端的连接请求,并返回一个新的套接字。
所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。
与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数据来完成的。
accept()函数仅被TCP类型的服务器程序调用,从已完成连接队列返回下一个建立成功的连接,
如果已完成连接队列为空,线程进入阻塞态睡眠状态。成功时返回套接字描述符,错误时返回-1。
如果accpet()执行成功,返回由内核自动生成的一个全新socket描述符,
用它引用与客户端的TCP连接。通常我们把accept()第一个参数成为监听套接字(listening socket),
把accept()功能返回值成为已连接套接字(connected socket)。
一个服务器通常只有1个监听套接字,监听客户端的连接请求;
服务器内核为每一个客户端的TCP连接维护1个已连接套接字,用它实现数据双向通信。
8、setsockopt()函数:
函数原型:int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
函数作用:设置与某个套接字关联的选项,例如:recv等函数默认为阻塞模式(block),即直到有数据到来之前函数不会返回,
而我们有时则需要一种超时机制使其在一定时间后返回而不管是否有数据到来,这里我们就会用到setsockopt()函数。
参数:
sock:将要被设置选项的套接字。
level:选项所在的协议层,可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项.
optname:指定控制的方式,根据level的不同,可以由如下配置方式:
选项名称 说明 数据类型
========================================================================
SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSERADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
========================================================================
IPPROTO_IP
------------------------------------------------------------------------
IP_HDRINCL 在数据包中包含IP首部 int
IP_OPTINOS IP首部选项 int
IP_TOS 服务类型
IP_TTL 生存时间 int
========================================================================
IPPRO_TCP
------------------------------------------------------------------------
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int
========================================================================
optval:根据optname的不同,这个参数也不一样,例如当optname为 SO_RCVTIMEO 超时接受时,optval可以为
struct timeval
{
time_t tv_sec;
time_t tv_usec;
};
这种类型的结构体,表示满足一定时间后退出。
optlen:optval的长度。
9、fcntl()函数:
函数原型:int fcntl(int fd, int cmd, long arg);
函数作用:可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性。
参数:fd,socket函数返回的描述符;
cmd,这里先用两个cmd举例,
F_GETFL:获取文件打开方式的标志。
F_SETF :设置文件打开方式为第三个参数arg指定方式
通常用这个函数来设置阻塞性质,举例:
int flags = fcntl(socket, F_GETFL, 0); //将文件描述符的标志保存到flags中
/* 设置为非阻塞*/
if (fcntl(socket_descriptor, F_SETFL, flags | O_NONBLOCK) < 0)
{
/* Handle error */
}
/* 设置为阻塞 */
if ((flags = fcntl(sock_descriptor, F_SETFL, 0)) < 0)
{
/* Handle error */
}