40 | 瞧一瞧Linux:详解socket的接口实现
下载APP
关闭
渠道合作
推荐作者
40 | 瞧一瞧Linux:详解socket的接口实现
2021-08-09 LMOS 来自北京
《操作系统实战45讲》
课程介绍
讲述:陈晨
时长09:56大小9.07M
你好,我是 LMOS。
上节课,我们一起了解了套接字的工作机制和数据结构,但套接字有哪些基本接口实现呢?相信学完这节课,你就能够解决这个问题了。
今天我会和你探讨套接字从创建、协议接口注册与初始化过程,还会为你深入分析套接字系统,是怎样调用各个功能函数的。通过这节课,相信你可以学会基于套接字来编写网络应用程序。有了之前的基础,想理解这节课并不难,让我们正式开始吧。
套接字接口
套接字接口最初是 BSD 操作系统的一部分,在应用层与 TCP/IP 协议栈之间接供了一套标准的独立于协议的接口。
Linux 内核实现的套接字接口,将 UNIX 的“一切都是文件操作”的概念应用在了网络连接访问上,让应用程序可以用常规文件操作 API 访问网络连接。
从 TCP/IP 协议栈的角度来看,传输层以上的都是应用程序的一部分,Linux 与传统的 UNIX 类似,TCP/IP 协议栈驻留在内核中,与内核的其他组件共享内存。传输层以上执行的网络功能,都是在用户地址空间完成的。
Linux 使用内核套接字概念与用户空间套接字通信,这样可以让实现和操作变得更简单。Linux 提供了一套 API 和套接字数据结构,这些服务向下与内核接口,向上与用户空间接口,应用程序正是使用这一套 API 访问内核中的网络功能。
套接字的创建
在应用程序使用 TCP/IP 协议栈的功能之前,我们必须调用套接字库函数 API 创建一个新的套接字,创建好以后,对库函数创建套接字的调用,就会转换为内核套接字创建函数的系统调用。
这时,完成的是通用套接字创建的初始化功能,跟具体的协议族并不相关。
这个过程具体是这样的,在应用程序中执行 socket 函数,socket 产生系统调用中断执行内核的套接字分路函数 sys_socketcall,在 sys_socketcall 套接字函数分路器中将调用传送到 sys_socket 函数,由 sys_socket 函数调用套接字的通用创建函数 sock_create。
sock_create 函数完成通用套接字创建、初始化任务后,再调用特定协议族的套接字创建函数。
这样描述你可能还没有直观感受,我特意画了图,帮你梳理 socket 创建的流程,你可以对照图片仔细体会调用过程。
socket创建示意图
结合图解,我再用一个具体例子帮你加深理解,比如由 AF_INET 协议族的 inet_create 函数完成套接字与特定协议族的关联。
一个新的 struct socket 数据结构起始由 sock_create 函数创建,该函数直接调用 __sock_create 函数,__sock_create 函数的任务是为套接字预留需要的内存空间,由 sock_alloc 函数完成这项功能。
这个 sock_alloc 函数不仅会为 struct socket 数据结构实例预留空间,也会为 struct inode 数据结构实例分配需要的内存空间,这样可以使两个数据结构的实例相关联。__sock_create 函数代码如下。
sock_alloc 函数如下所示。
当具体的协议与新套接字相连时,其内部状态的管理由协议自身维护。
现在,函数将 struct socket 数据结构的 struct proto_ops *ops 设置为 NULL。随后,当某个协议族中的协议成员的套接字创建函数被调用时,ops 将指向协议实例的操作函数。这时将 struct socket 数据结构的 flags 数据域设置为 0,创建时还没有任何标志需要设置。
在之后的调用中,应用程序调用 send 或 receive 套接字库函数时会设置 flags 数据域。最后将其他两个数据域 sk 和 file 初始化为 NULL。sk 数据域随后会把由协议特有的套接字创建函数设置为指向内部套接字结构。file 将在调用 sock_ma_fd 函数时设置为分配的文件返回的指针。
文件指针用于访问打开套接字的虚拟文件系统的文件状态。在 sock_alloc 函数返回后,sock_create 函数调用协议族的套接字创建函数 err =pf->create(net, sock, protocol),它通过访问 net_families 数组获取协议族的创建函数,对于 TCP/IP 协议栈,协议族将设置为 AF_INET。
套接字的绑定
创建完套接字后,应用程序需要调用 sys_bind 函数把套接字和地址绑定起来,代码如下所示。
结合代码,我们可以看到,sys_bind 函数首先会查找套接字对应的 socket 实例,调用 sockfd_lookup_light。在绑定之前,将用户空间的地址拷贝到内核空间的缓冲区中,在拷贝过程中会检查用户传入的地址是否正确。
等上述的准备工作完成后,就会调用 inet_bind 函数来完成绑定操作。inet_bind 函数代码如下所示。
主动连接
因为应用程序处理的是面向连接的网络服务(SOCK_STREAM 或 SOCK_SEQPACKET),所以在交换数据之前,需要在请求连接服务的进程(客户)与提供服务的进程(服务器)之间建立连接。
当应用程序调用 connect 函数发出连接请求时,内核会启动函数 sys_connect,详细代码如下。
连接成功会返回 socket 的描述符,否则会返回一个错误码。
监听套接字
调用 listen 函数时,应用程序触发内核的 sys_listen 函数,把套接字描述符 fd 对应的套接字设置为监听模式,观察连接请求。详细代码你可以看看后面的内容。
被动接收连接
前面说过主动连接,我们再来看看被动接受连接的情况。接受一个客户端的连接请求会调用 accept 函数,应用程序触发内核函数 sys_accept,等待接收连接请求。如果允许连接,则重新创建一个代表该连接的套接字,并返回其套接字描述符,代码如下。
这个新的套接字描述符与最初创建套接字时,设置的套接字地址族与套接字类型、使用的协议一样。原来创建的套接字不与连接关联,它继续在原套接字上侦听,以便接收其他连接请求。
发送数据
套接字应用中最简单的传送函数是 send,send 函数的作用类似于 write,但 send 函数允许应用程序指定标志,规定如何对待传送数据。调用 send 函数时,会触发内核的 sys_send 函数,把发送缓冲区的数据发送出去。
sys_send 函数具体调用流程如下。
1. 应用程序的数据被复制到内核后,sys_send 函数调用 sock_sendmsg,依据协议族类型来执行发送操作。
2. 如果是 INET 协议族套接字,sock_sendmsg 将调用 inet_sendmsg 函数。
3. 如果采用 TCP 协议,inet_sendmsg 函数将调用 tcp_sendmsg,并按照 TCP 协议规则来发送数据包。
send 函数返回发送成功,并不意味着在连接的另一端的进程可以收到数据,这里只能保证发送 send 函数执行成功,发送给网络设备驱动程序的数据没有出错。
接收数据
recv 函数与文件读 read 函数类似,recv 函数中可以指定标志来控制如何接收数据,调用 recv 函数时,应用程序会触发内核的 sys_recv 函数,把网络中的数据递交到应用程序。当然,read、recvfrom 函数也会触发 sys_recv 函数。具体流程如下。
1. 为把内核的网络数据转入应用程序的接收缓冲区,sys_recv 函数依次调用 sys_recvfrom、sock_recvfrom 和 __sock_recvmsg,并依据协议族类型来执行具体的接收操作。
2. 如果是 INET 协议族套接字,__sock_recvmsg 将调用 sock_common_recvmsg 函数。
3. 如果采用 TCP 协议,sock_common_recvmsg 函数将调用 tcp_recvmsg,按照 TCP 协议规则来接收数据包
如果接收方想获取数据包发送端的标识符,应用程序可以调用 sys_recvfrom 函数来获取数据包发送方的源地址,下面是 sys_recvfrom 函数的实现。
关闭连接
最后,我们来看看如何关闭连接。当应用程序调用 shutdown 函数关闭连接时,内核会启动函数 sys_shutdown,代码如下。
重点回顾
好,这节课的内容告一段落了,我来给你做个总结。这节课我们继续研究了套接字在 Linux 内核中的实现。
套接字是 UNIX 兼容系统的一大特色,Linux 在此基础上实现了内核套接字与应用程序套接字接口,在用户地址空间与内核地址空间之间提供了一套标准接口,实现应用套接字库函数与内核功能之间的一一对应,简化了用户地址空间与内核地址空间交换数据的过程。
通过应用套接字 API 编写网络应用程序,我们可以利用 Linux 内核 TCP/IP 协议栈提供的网络通信服务,在网络上实现应用数据快速、有效的传送。除此之外,套接字编程还可以使我们获取网络、主机的各种管理、统计信息。
创建套接字应用程序一般要经过后面这 6 个步骤。
1. 创建套接字。
2. 将套接字与地址绑定,设置套接字选项。
3. 建立套接字之间的连接。
4. 监听套接字
5. 接收、发送数据。
6. 关闭、释放套接字。
思考题
我们了解的 TCP 三次握手,发生在 socket 的哪几个函数中呢?
欢迎你在留言区跟我交流,也推荐你把这节课转发给有需要的朋友。
我是 LMOS,我们下节课见!
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 4
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
39 | 瞧一瞧Linux:详解socket实现与网络编程接口
下一篇
41 | 服务接口:如何搭建沟通桥梁?
精选留言(9)
- neohope置顶2021-08-15四次挥手过程分析下【V5.8,正常流程】 5、客户端收到FIN包,子状态从TCP_FIN_WAIT2变为TCP_TIME_WAIT,返回ACK包 A、状态和子状态都为TCP_TIME_WAIT 【tcp_protocol.handler】tcp_v4_rcv-> ->if (sk->sk_state == TCP_TIME_WAIT) goto do_time_wait; ->do_time_wait: ->tcp_timewait_state_process ->->if (tw->tw_substate == TCP_FIN_WAIT2) ->->tw->tw_substate = TCP_TIME_WAIT; ->->inet_twsk_reschedule,重新设置回调时间 ->->return TCP_TW_ACK; B、返回ACK ->case TCP_TW_ACK: ->tcp_v4_timewait_ack(sk, skb); 6、服务端收到ACK包,状态从TCP_LAST_ACK变为TCP_CLOSE 【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process ->case TCP_LAST_ACK: ->tcp_done ->->tcp_set_state(sk, TCP_CLOSE); 7、客户端超时回调 A、超时时间定义 #define TCP_TIMEWAIT_LEN (60*HZ) #define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN B、超时后,回调tw_timer_handler->inet_twsk_kill,进行inet_timewait_sock清理工作 C、没有找到状态变从TCP_TIME_WAIT变为TCP_CLOSE的代码 D、只看没调,有问题的,欢迎小伙伴告诉一下展开
作者回复: 66666
4 - neohope置顶2021-08-15四次挥手过程分析上【V5.8,正常流程】 1、客户端主动断开连接,状态从TCP_ESTABLISHED变为TCP_FIN_WAIT1,发送FIN包给服务端 A、状态变为TCP_FIN_WAIT1 tcp_close->tcp_close_state ->tcp_set_state(sk, new_state[TCP_ESTABLISHED]),也就是TCP_FIN_WAIT1 B、发送FIN包 tcp_close->tcp_close_state ->tcp_send_fin 2、服务端收到FIN包,状态从TCP_ESTABLISHED变为TCP_CLOSE_WAIT,并返回ACK包 A、状态变为TCP_CLOSE_WAIT 【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established ->tcp_data_queue ->->tcp_fin ->->->inet_csk_schedule_ack; 安排ack ->->->sk->sk_shutdown |= RCV_SHUTDOWN; 模拟了close ->->->sock_set_flag(sk, SOCK_DONE); ->->->case TCP_ESTABLISHED: ->->->tcp_set_state(sk, TCP_CLOSE_WAIT); 修改状态 ->->inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW; ACS是否立即发送 B、发送ACK包 【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established【接上面】 ->tcp_ack_snd_check->__tcp_ack_snd_check->tcp_send_ack 3、客户端收到ACK包,状态从TCP_FIN_WAIT1变为TCP_FIN_WAIT2,然后被替换为状态TCP_TIME_WAIT,子状态TCP_FIN_WAIT2 【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process ->case TCP_FIN_WAIT1: ->tcp_set_state(sk, TCP_FIN_WAIT2); ->tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); ->->tw = inet_twsk_alloc(sk, tcp_death_row, state); ->->->tw->tw_state = TCP_TIME_WAIT; ->->->tw->tw_substate = TCP_FIN_WAIT2; ->->->timer_setup(&tw->tw_timer, tw_timer_handler, TIMER_PINNED); 4、服务端状态从TCP_CLOSE_WAIT变为TCP_LAST_ACK,发送FIN包 A、状态变为TCP_LAST_ACK tcp_close->tcp_close_state ->tcp_set_state(sk, new_state[TCP_CLOSE_WAIT]),也就是TCP_LAST_ACK B、发送FIN包 tcp_close->tcp_close_state ->tcp_send_fin展开2
- neohope置顶2021-08-14三次握手过程分析【V5.8,正常流程】 1、客户端发起第一次握手,状态调变为TCP_SYN_SENT,发送SYN包 connect->__sys_connect->__sys_connect_file->【sock->ops->connect】tcp_v4_connect A、状态变化 ->tcp_set_state(sk, TCP_SYN_SENT); B、发送SYN ->tcp_connect->tcp_send_syn_data 2、服务端收到客户端的SYN包,初始化socket,状态从TCP_LISTEN变为TCP_NEW_SYN_RECV,发送第二次握手SYN_ACK包 A、收到连接,初始化socket accept->__sys_accept4->__sys_accept4_file->【sock->ops->accept】inet_csk_accept B、收到SYN,改变状态 【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process-> ->case TCP_LISTEN: ->[sock->ops->conn_request]tcp_v4_conn_request->tcp_conn_request ->->inet_reqsk_alloc ->->->ireq->ireq_state = TCP_NEW_SYN_RECV; C、发送SYN_ACK包 ->[sock->ops->conn_request]tcp_v4_conn_request->tcp_conn_request【和B路径一样】 ->->【af_ops->send_synack】tcp_v4_send_synack ->->->tcp_make_synack ->->->__tcp_v4_send_check 3、客户端收到SYN_ACK包,状态从TCP_SYN_SENT变为TCP_ESTABLISHED,并发送ACK包 A、收到SYN_ACK包 【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process ->case TCP_SYN_SENT: ->tcp_rcv_synsent_state_process->tcp_finish_connect ->->tcp_set_state(sk, TCP_ESTABLISHED); B、发送ACK包 ->tcp_rcv_synsent_state_process->tcp_send_ack->__tcp_send_ack 4、服务端收到ACK包,状态从TCP_NEW_SYN_RECV变为TCP_SYN_RECV【实际上是新建了一个sock】 【tcp_protocol.handler】tcp_v4_rcv-> ->if (sk->sk_state == TCP_NEW_SYN_RECV) ->tcp_check_req ->->【inet_csk(sk)->icsk_af_ops->syn_recv_sock】tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone_lock ->->->inet_sk_set_state(newsk, TCP_SYN_RECV); 5、服务端状态从TCP_SYN_RECV变为TCP_ESTABLISHED 【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process ->case TCP_SYN_RECV: ->tcp_set_state(sk, TCP_ESTABLISHED); 只看没调,有问题的欢迎各位小伙伴指出。展开
作者回复: 是的 总结到位
3 - MacBao2021-08-09服务器端处于listen状态,客户端connect发起TCP三次握手?1
- pedro2021-08-09今天的问题不好回答,因为文中无明显三次握手的代码,而且三次握手的机制其实比较复杂,涉及到几个状态和几个队列之间的切换,笼统的 connect 和 accept 函数是说不清楚的,感兴趣可以看看这里: https://blog.csdn.net/tennysonsky/article/details/45621341 当然这些不能全信,所以还是得自己看linux内核代码,待我看了再来补充😂展开
作者回复: 好的 期待
共 3 条评论2 - 王子虾2022-04-09老师,有一个问题,tcp在调用listen的时候,有全连接队列的概念,一般上限是128。但是问题是,我们比如实现单机百万链接的时候,一个server端的源组(server_ip+port),比如有65535个client,那会不会受限于这个全连接队列?
作者回复: 会,但 linux可以修改 /proc/sys/net/core/somaxconn
- ifelse2022-02-25nice
作者回复: good
- GeekCoder2021-10-24能讲讲epoll吗?
作者回复: 本课程是os实现课程 不会讲到这个
- pedro2021-08-09这里有一篇三次握手的源码图解:https://mp.weixin.qq.com/s/vlrzGc5bFrPIr9a7HIr2eA
作者回复: 谢谢 老铁