Socket 系统调用深入研究(TCP协议的整个通信过程)

说明

本文主要参考的原文:Know your TCP system call sequences
socket api可以参考我的博客:socket API 介绍
TCP DEMO:tcp demo
注意:本文主要是翻译文章,因此会存在翻译难懂或者翻译有问题的地方,请自行阅读原文更正

内核态和用户态

通俗的说,用户空间就是运行着用户编写的应用程序的虚拟内存空间。在32位的操作系统中,每个进程都有 4GB 独立的虚拟内存空间,而 0 ~ 3GB 的虚拟内存空间就是用户空间 。

内核空间就是运行着操作系统代码的虚拟内存空间,而 3GB ~ 4GB 的虚拟内存空间就是内核空间。

在Linux中(当然windows下也有用户态和内核态的区分,不过一般说用户态和内核态针对的事Linux系统而已),为了更好地保护内核空间,将程序运行空间分为内核空间和用户空间(也就是常说的内核态和用户态),它们分别运行在不同的级别上,逻辑上相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,他们只能在用户空间访问用户数据,调用用户空间的函数。

但是,在有些情况下,用户空间的进程需要的进程需要获得一定的系统服务(调用内核空间的程序),这时操作系统就必须调用系统为用户提供的“特殊接口”-系统调用规定用户进程进入内核空间的具体位置。进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完后在返回内核空间。这就涉及到了”系统调用”。

系统调用

系统调用是操作系统提供给用户程序调用的一组“特殊”编程接口,用户程序可以通过这组“特殊”接口获得操作系统内核提供的服务。通常,我们可以将这组“特殊”的接口称之为内核API.

系统调用按照功能逻辑可以分为:进程控制,进程间通信,文件系统控制,系统控制,存储管理,网络管理,socket控制,用户管理等。

用户编程接口API

通常,在日常的软件开发中,系统调用不直接与程序员进行交互,它仅仅是一个软中断机制向内核提交请求以获得内核服务的接口。实际使用中程序员调用的通常是用户编程接口-API,然后在由用户编程接口API去通知内核完成相应的系统调用。

例如,获取进程号的API函数getpid()对应getpid系统调用。但并不是所有的函数都对应一个系统调用,有时,一个API函数会需要几个系统调用来共同完成函数的功能,甚至有一些API函数不需要相应的系统调用(因此它所完成的不是内核提同的服务)。

在Linux中用户编程接口(API)遵循了在UNIX中最流行的应用编程界面标准-POSIX标准。

这里博客的主要内容不是介绍linux系统的API,而主要介绍linux系统中涉及到网络API(也是基于POSIX标准的网络API),如果对socket API不熟悉的同学可以参考我的博客

词汇解释

为了方便后续的说明,这里先对涉及到的一些概念进行说明。

通信五元组

服务器与客户端的通信是通过五元组来区分的,五元组的元素通常是指源IP地址,源端口,目的IP地址,目的端口和传输层协议(TCP/UDP).五元组能够区分不同会话,并且对应的会话是唯一的。通过五元组我们就可以实现一次找到一次通信的来源和去处。

TCB控制块

TCB是什么东西,关于TCB的资料不太多,TCB在整个TCP生命周期具体有什么作用,我也不是很清楚,先来看看TCB结构的定义。

struct tcb {
	short	tcb_state;	/* TCP state	TCP状态(11种,比如LISTEN状态)		*/
	short	tcb_ostate;	/* output state				*/
	short	tcb_type;	   /* TCP type (SERVER, CLIENT)		*/
	int	tcb_mutex;	   /* tcb mutual exclusion			*/
	short	tcb_code;	/* TCP code for next packet		*/
	short	tcb_flags;	    /* various TCB state flags		*/
	short	tcb_error;	/* return error for user side		*/
	
    //五元组信息
	IPaddr	tcb_rip;	/* remote IP address			*/
	u_short	tcb_rport;	/* remote TCP port			*/
	IPaddr	tcb_lip;	/* local IP address			*/
	u_short	tcb_lport;	/* local TCP port			*/
	struct	netif	*tcb_pni; /* pointer to our interface		*/

	tcpseq	tcb_suna;	   /* send unacked				*/
	tcpseq	tcb_snext;	/* send next				*/
	tcpseq	tcb_slast;	/* sequence of FIN, if TCBF_SNDFIN	*/
	u_long	tcb_swindow;	/* send window size (octets)		*/
	tcpseq	tcb_lwseq;	/* sequence of last window update	*/
	tcpseq	tcb_lwack;	/* ack seq of last window update	*/
	u_int	tcb_cwnd;	/* congestion window size (octets)	*/
	u_int	tcb_ssthresh;	/* slow start threshold (octets)	*/
	u_int	tcb_smss;	/* send max segment size (octets)	*/
	tcpseq	tcb_iss;	/* initial send sequence		*/
	int	tcb_srt;	/* smoothed Round Trip Time		*/
	int	tcb_rtde;	/* Round Trip deviation estimator	*/
	int	tcb_persist;	/* persist timeout value		*/
	int	tcb_keep;	/* keepalive timeout value		*/
	int	tcb_rexmt;	/* retransmit timeout value		*/
	int	tcb_rexmtcount;	/* number of rexmts sent		*/

	tcpseq	tcb_rnext;	/* receive next				*/
	tcpseq	tcb_rupseq;	/* receive urgent pointer		*/
	tcpseq	tcb_supseq;	/* send urgent pointer			*/

	int	tcb_lqsize;	/* listen queue size (SERVERs)		*/
	int	tcb_listenq;	/* listen queue port (SERVERs)		*/
	struct tcb *tcb_pptcb;	/* pointer to parent TCB (for ACCEPT)	*/
	int	tcb_ocsem;	/* open/close semaphore 		*/
	int	tcb_dvnum;	/* TCP slave pseudo device number	*/

	int	tcb_ssema;	/* send semaphore			*/
	u_char	*tcb_sndbuf;	/* send buffer				*/
	u_int	tcb_sbstart;	/* start of valid data			*/
	u_int	tcb_sbcount;	/* data character count			*/
	u_int	tcb_sbsize;	/* send buffer size (bytes)		*/

	int	tcb_rsema;	/* receive semaphore			*/
	u_char	*tcb_rcvbuf;	/* receive buffer (circular)		*/
	u_int	tcb_rbstart;	/* start of valid data			*/
	u_int	tcb_rbcount;	/* data character count	 接收数据长度		*/
	u_int	tcb_rbsize;	/* receive buffer size (bytes) 接收缓冲区大小		*/
	u_int	tcb_rmss;	/* receive max segment size		*/
	tcpseq	tcb_cwin;	/* seq of currently advertised window 记录了当前窗口可接收的最大报文段序号	*/
	int	tcb_rsegq;	/* segment fragment queue		*/
	tcpseq	tcb_finseq;	/* FIN sequence number, or 0		*/
	tcpseq	tcb_pushseq;	/* PUSH sequence number, or 0		*/
	};

TCB(Transmission Control Block,传输控制块),基本上网上对于这个说法如下:socket包含两个成分,一个是IP地址,一个是端口号。同一个设备可以对应一个IP端口,但不同的“水管”用不同的端口号区分开来,于是同一个设备发送给其他不同设备的信息就不会产生混乱。在同一时刻,设备可能会产生多种数据需要分发给不同的设备,为了确保数据能够正确分发,TCP用一种叫做TCB,也叫传输控制块的数据结构把发给不同设备的数据封装起来,我们可以把该结构看做是信封。一个TCB数据块包含了数据发送双方对应的socket信息以及拥有装载数据的缓冲区。在两个设备要建立连接发送数据之前,双方都必须要做一些准备工作,分配内存建立起TCB数据块就是连接建立前必须要做的准备工作。

如果相对TCB控制块结构的每个参数都需要了解,那么可能需要参考更专业的书籍以及对TCP有更深入的学习才可以,不过可以tcb_state字段知道TCB具有整个TCP过程的整个生命周期,具体的来说就是从socket创建到socket的关闭整个过程,因此TCB的创建是在socket函数进行创建的,结束于close函数。随着时间的推移,TCB结构的各个参数也会发生变化。

TCP通信流程

在Linux系统中,一切皆文件,Socket也不例外,对socket的操作实际上也是对某种特殊文件的读写操作。只不过操作的文件在服务器和客户端各自拥有一个。

内核操作API

在介绍socket系统调用之前,先介绍一些内核API。
**fget_light()和fput_light():**轻量级的文件查找入口。多任务对同一个文件进行操作,所以需要对文件做引用计数。fget_light在当前进程的struct files_struct中根据所谓的用户空间文件描述符fd来获取文件描述符。另外,根据当前fs_struct是否被多各进程共享来判断是否需要对文件描述符进行加锁,并将加锁结果存到一个int中返回, fput_light则根据该结果来判断是否需要对文件描述符解锁。
fget_light()/fput_light是fget/fput的变形,不用考虑多进程共享同一个文件表而导致的竞争避免锁。

fget/fput:指在文件表的引用计数+1/-1

sockfd_lookup_light:根据fd找到相应的socket object(内核真正操作的对象)。

so_xxx: 内核相关socket操作接口。socket object操作协议栈的api入口。

in_pcballoc():分配内核内存,内存名字叫Internet protocol control block。

in_pcbbind():绑定IN_PCB到指定的地址,如果不指定地址,那么会寻找一个可用的端口进行绑定

in_pcblookup():指定的端口是否可用。

sbappend():追加数据到发送缓冲区。

so->so_proto->pr_usrreq: socket object操作协议栈的函数

tcp_ursreq():是tcp 协议栈操作的入口函数,支持以下操作类型:PRU_ATTACH,PRU_BIND,PRU_LISTEN, PRU_ACCEPT, PRU_CONNECT, PRU_SHUTDOWN,PRU_ABORT, PRU_DETACH,PRU_SEND,PRU_SENDOOB,PRU_RCVD,PRU_RCVOOB

tcp_newtcpcb():TCP control block被分配,socket描述符指向的正是这个TCP control block。

tcp_attach().

tcp_xxx: tcp_close(), tcp_disconect(),tcp_drop()

pr_xxx: 一套socket层和协议栈通信的接口,包括pr_usrreq(),pr_input(),pr_output(),pr_ctlinput(),pr_ctloutput()。

TCP系统调用

Socket 系统调用深入研究(TCP协议的整个通信过程)_第1张图片
上图显示了 TCP 系统调用在物理链路上发出之前进行传播的各个层。,我们一些列的API操作都只是在用户态(Process)进行,套接字层(Socket layer)接收进行的任何TCP系统调用,验证 TCP 应用程序传递的参数的正确性。这是一个独立于协议的层,因为尚未将协议连接到调用中。

套接字层下面是协议层(Protocol layer),该层包含协议的实际实现(本例中为 TCP)。当套接字层对协议层进行调用时,将确保对两个层之间共享的数据结构具有独占访问权限。这样做是为了避免任何数据结构损坏。

各种网络设备驱动程序在接口层(Interface layer)运行,该层从物理链路接收数据,并向物理链路传输数据。

每个套接字都有一个套接字队列,每个接口都有一个用于数据通信的接口队列。然而,整个协议层只有一个协议队列,称为IP输入队列。接口层通过这个IP输入队列向协议层输入数据。协议层使用各自的接口队列向接口输出数据。

握手之前

在客户端没有连接到服务器之前,服务器需要做一些初始化工作,以便能够监听客户端的连接,主要包括创建socket,将socket绑定到固定的端口和地址(可选,可以绑定到本机的任意地址),最后监听客户端的连接,涉及到的用户编程接口API包括socket,bind,listen.

socket

在我们调用socket 编程接口API之后,是在内核创建了一个socket对象,并返回对象的引用fd,通过这个fd我们可以操作这个socket。

socket在系统调用的表现形式如下:

socket (struct proc *p, struct socket_args *uap, int retval)
struct sock_args
{
int domain,
int type,
int protocol;
};

p是一个指针,指向进行套接字调用的进程的proc结构。
uap是指向socket_args结构的指针,该结构包含在socket系统调用中传递给进程的参数。
retval是系统调用的返回值。

socket系统调用通过分配一个新的描述符来创建一个新的socket。新描述符将返回给调用进程。任何后续的系统调用都用创建的套接字标识。socket系统调用还将协议分配给创建的套接字描述符。
domain、type和protocol参数值指定要分配给所创建套接字的族、类型和协议(即我们调用socket API传递的参数)。下图显示了socket系统调用的过程。
Socket 系统调用深入研究(TCP协议的整个通信过程)_第2张图片

一旦从进程中检索到参数,socket函数就会调用socreate函数。socreate函数根据进程指定的参数查找指向协议切换protsw结构的指针。然后,socreate函数分配一个新的套接字结构。然后进行协议特定的调用 pr_usrreq,进而切换到与套接字描述符关联的相应协议特定的请求。pr_usrreq 函数的原型为:

int  pr_usrreq(struct socket ∗so , int req, struct mbuf  ∗m0 , ∗m1 , ∗m2);

在 pr_usrreq 函数中:
so是指向套接字结构的指针。
req的功能是标识请求。本例中为 PRU_ATTACH。
m0、m1和m2是指向mbuf结构的指针。值因请求而异。

pr_usrreq功能为大约16个请求提供服务。

tcp_usrreq()函数调用了tcp_attach( ),以处理PRU_ATTACH请求。为了分配Internet协议控制块(Internet protocol control block),需要调用in_pcballoc()。在in_pcballoc()中,调用了内核的内存分配器函数,该函数将内存分配给Internet控制块。完成所有必要的Internet控制块结构指针初始化之后,该控制返回到tcp_attach()。

分配新的TCP控制块(TCB),并调用tcp_newtcpcb()进行初始化。它还初始化所有TCP定时器变量,控制返回给tcp_attach()。套接字状态初始化为CLOSED。,tcp_usrreq函数返回时,套接字描述符将指向套接字的tcp控制块(TCB)。

Internet控制块是双向的循环链表,其指针指向套接字结构,同时套接字结构的so_pcb字段指向Internet控制块结构。Internet控制块还具有指向TCP控制块的指针。

bind

bind的系统调用如下,其中的uap参数即是我们通过bind API传递给内核层的参数。

bind (struct proc ∗p, struct bind_args ∗uap, int ∗retval)
   struct bind_args 
   {   int s;
       caddr_t name;
       int namelen;
   };

s是套接字描述符。
name是指向包含网络传输地址的缓冲区的指针。
namelen是缓冲区的大小。

bind系统调用将本地网络传输地址与套接字相关联。对于客户端进程,不需要强制bind调用。当客户端进程发出connect系统调用时,内核负责执行隐式绑定。服务器进程在接受连接或开始与客户端通信之前,通常需要发出显式bind请求。

bind 调用将进程指定的本地地址复制到 mbuf,并调用 sobind,后者则根据请求使用 PRU_BIND 调用 tcp_usrreq()。tcp_usrreq() 中的切换实例调用 in_pcbbind(),后者将本地地址和端口号绑定到套接字。in_pcbbind 函数首先执行一些完整性检查,以确保不绑定套接字两次,并且保证其绑定了一个地址(存在多网卡的情况)。in_pcbbind 负责隐式和显式绑定。

如果调用in_pcbbind()中的第二个参数(指向sockaddr_in结构的指针)非空,则会发生显式绑定。否则会发生隐式绑定。在显式绑定的情况下,将对绑定的IP地址执行检查,并相应地设置套接字选项。
bind系统调用的流程如下:
Socket 系统调用深入研究(TCP协议的整个通信过程)_第3张图片

如果指定的本地端口为非零值,则如果绑定位于保留端口上(例如,根据Berkley约定,端口号<1024),则会检查超级用户权限。然后调用in_pcblookup()来查找具有所述本地IP地址和本地端口号的控制块。in_pcblookup()验证本地地址和端口对是否尚未使用。如果in_pcbbind()中的第二个参数为NULL或本地端口为零,则通过检查找到临时端口(例如,根据Berkley约定,端口号>1024且<5000)。然后调用in_pcblookup()来验证找到的端口是否未使用。

listen

listen (struct proc ∗p, struct listen_args ∗uap, int ∗retval)
struct listen_args
{ int s;
   int backlog;
};

在listen系统调用中

s是套接字描述符。
backlog是套接字上连接数的队列限制。

listen 调用指示协议,服务器进程准备接受套接字上任何新传入的连接。存在一个可以排列的连接数限制,在该连接数之后,忽略任何进一步的连接请求。

关于backlog参数后面介绍完三次握手之后还会继续说明在不同系统中,这个参数的作用。

listen 系统调用使用套接字描述符和 listen 调用中指定的backlog 值调用 solisten。solisten 仅使用 PRU_LISTEN 作为请求调用 tcp_usrreq 函数。在 tcp_usrreq() 函数的switch语句中,PRU_LISTEN 的实例检查套接字是否绑定到端口。如果端口为零,则调用 in_pcbbind(),将套接字绑定到一个端口
如果端口上已经有侦听套接字,则该套接字的状态将更改为LISTEN。通常,所有服务器进程都会侦听一个已知的端口号。调用in_pcbbind为服务器进程执行隐式绑定是非常罕见的。listen系统调用流程如下。
Socket 系统调用深入研究(TCP协议的整个通信过程)_第4张图片

TCP三次握手

在TCP握手的过程中,编程接口包括connect和accept接口,前者用户客户端连接到服务器,后者用于服务器接受客户端的连接,在具体介绍系统调用之前,先来讲一下三次握手,关于三次握手网上也有很多资料。我这里也是参考了大量的博客,个人觉得比较好的来讲解。
当服务器端创建好socket,并调用了bind和listen之后,服务器就处于监听状态,随时监听客户端的连接,握手过程如下图所示。

当客户端调用connect函数之后,服务器和客户端之间就开启了握手,在服务器端会维护2个连接队列,一个是半连接队列(又称为SYN 队列),另一个是全连接队列(又称为全连接队列)。第一次握手的时候,客户端会携带客户端的信息(地址和端口)以及服务器的信息(要连接的服务器地址和端口等)以及会发送一个同步序号(SYN)给服务器,这个序号告诉服务器需要使用这个序号+1来同我进行同步,服务器接着会创建一个socket(这个socket信息不完整,不能进行通信,不过该socket具有TCB控制块信息,能够存放TCP状态信息),并将这个socket信息放入到半连接队列,此时客户端的TCP状态为SYN_SET,服务器端的TCP状态为SYN_RECV,服务器随即给客户端发送一个ACK(作为对客户端请求的回应,序号值ACK=SYN+1,注意这个SYN为客户端发给服务器的SYN序号),并且服务器自身也会发送一个SYN序号给服务器,客户端收到ACK和SYN后,比对ACK是否自己发送的SYN+1,如果是,则代表这是对我连接的回应,此时客户端的状态为ESTABLISHED,这时候告知客户端可以接收到服务器的信息,然而由于在网络环境比较复杂的情况,客户端可能会连续发送多次请求。如果只设计成两次握手的情况,服务端只能一直接收请求,然后返回请求信息,也不知道客户端是否请求成功。这些过期请求的话就会造成网络连接的混乱,因此这时候客户端还需要发送第三次握手通知服务器,此时服务器端的TCP状态也处于ESTABLISHED。这样服务器与客户端就建立起了连接,三次握手完成的时候,服务器刚刚创建的socket信息就是一个完整的socket信息(能够和客户端进行通信),并且将该socket信息从半连接队列中移除,并加入到全连接队列中,这时候accept就会从全队列中取出一个socket信息,这个socket信息负责和客户端进行通信。

connect

connect (struct proc ∗p, struct connect_args ∗uap, int ∗retval);
struct connect_args 
{
    int s;
    caddr_t name;
    int namelen;
};

s是套接字描述符。
name是指向服务器端IP/端口地址对的缓冲区的指针。
namelen是缓冲区的长度。

connect系统调用通常由客户端进程调用,以连接到服务器进程。如果客户端进程在启动连接之前没有显式发出bind系统调用,则本地套接字上的隐式绑定由堆栈负责。

connect系统调用将外部地址(连接请求需要发送到的地址)从进程复制到内核,并调用soconnect()。从soconnect()返回时,connect()函数会发出一个睡眠,直到协议层将其唤醒,这表明连接已建立(对于阻塞的fd而言,当三次握手的第二次握手完成时,connect就被唤醒)或套接字上出现了一些错误。soconnect()检查套接字的有效状态,并调用pr_usrreq(),请求时使PRU_CONNECT。

tcp_usrreq()函数中的switch case检查本地端口与套接字的绑定。如果套接字尚未绑定,则调用in_pcbbind(),执行隐式绑定。然后调用in_pcbconnect(),它获取到目的地的路由,找到必须输出数据包的接口,并验证connect()指定的外部套接字对(IP地址和端口号)是否唯一。然后,它用外部IP地址和端口号更新其Internet控制块,并返回到PRU_CONNECT case语句。

tcp_usrreq()调用soisconnecting(),它将客户端主机上套接字的状态设置为SYN_SENT。调用函数tcp_output,将SYN数据包输出到网络上。控制返回到connect()函数,该函数将一直休眠,直到协议层被唤醒——这表明连接现在已建立,或者套接字上出现了错误。

connect系统调用流程如下:
Socket 系统调用深入研究(TCP协议的整个通信过程)_第5张图片

accept

accept(struct proc ∗p, struct accept_args ∗uap, int ∗retval);
 struct  accept_args 
{
    int s;
    caddr_t name;
    int ∗anamelen;
};

在accpet系统调用中:

s是套接字描述符。
name是一个缓冲区(OUT参数),它包含外部主机的网络传输地址。
anamelen是名称缓冲区的大小。

accept系统调用是等待传入连接的阻塞调用。处理连接请求后,accept将返回一个新的套接字描述符。此新套接字已连接到客户端,而其他套接字仍处于侦听状态以接受进一步的连接。

accept系统调用过程如下:
Socket 系统调用深入研究(TCP协议的整个通信过程)_第6张图片
accept调用首先验证参数,并等待连接请求到达。在此之前,该功能会在while循环中阻塞。一旦新连接到达,协议层就会唤醒服务器进程。Accept然后检查阻塞时可能发生的任何套接字错误。如果有任何套接字错误,该函数将返回,并通过从队列中拾取新连接并调用soaccept进一步进行操作。在soaccept()中调用tcp_usrreq()函数,请求为PRU_ACCEPT。tcp_usrreq函数中的可选系统调用in_setpeeraddr()可以从协议控制块复制外部IP地址和外部端口号,并将它们返回给服务器进程。

注意:在我们调用accept编程接口可以传递一个大于0的backlog参数,这个参数在不同系统下可能具有不同含义,在mac系统下,这个参数可能是半连接和全连接的总数,此时可以通过设置这个参数可以对DDOS攻击有一定的作用(因为这个参数的大小对半连接的数量有限制作用,半连接就是客户端和服务器的第一次连接),然而在linux系统下,这个参数代表全连接队列的最大数量,因此设置这个参数的大小,对于DDOS攻击无能为力,此时可以通过设置反向代理的方式来阻止DDOS攻击。

TCP数据收发

客户端与服务器建立连接后,服务器与客户端之间就可以进行数据的收发了,我们在最开始学习网络编程的时候,以为调用send/write接口,就是将数据从一端发送了给了另一端。或者调用recv/read接口就是请求从另一端获取数据,其实这2种想法都是错误的。
Socket 系统调用深入研究(TCP协议的整个通信过程)_第7张图片
上图简单的表示了send,recv等读写接口都只作用于用户态,send的作用仅仅是将要发送的数据拷贝到内核态的发送缓冲区,内核具体什么时候发送缓冲区数据我们不知道,对于recv而言,我们也仅仅是将内核的接收缓冲区中拷贝数据到用户态。

注意:这里有很多知识点,比如零拷贝(sendfile)技术。这些面试的时候有的考官喜欢问。

sendmsg

sendmsg ( struct proc∗p, struct sendmsg_args ∗uap, int retval);
struct sendmsg_args
{    
   int s;
   caddr_t msg;
   int flags;
};

在send系统调用中

s是套接字描述符。
msg是指向msghdr结构的指针。
flags是控制信息。

有四个系统调用在n/w接口上发送数据:write、writev、sendto和sendmsg。本文只讨论sendmsg()系统调用。所有四个send调用最终都会调用sosend()。send(进程调用的库函数)、sendto和sendmsg系统调用只能在套接字描述符上操作,write和writev系统调用可以在任何类型的描述符上操作。

sendmsg系统调用流程如下:
Socket 系统调用深入研究(TCP协议的整个通信过程)_第8张图片
sendmsg系统调用将要从进程发送的消息复制到内核空间,并调用sendit()。在sendit()中,初始化一个结构,将进程的输出收集到内核的内存缓冲区中。地址和控制信息也会从进程复制到内核。然后调用sosend(),它执行四项任务:

(1)根据sendit()函数传递的值初始化各种参数。
(2)验证套接字的条件和连接的状态,并确定传递消息和报告错误所需的空间。
(3)分配内存并从进程中复制数据。
(4)进行特定于协议的调用,将数据发送到网络。

然后调用 tcp_usrreq(),并根据进程指定的标志,控制切换到 PRU_SEND 或 PRU_SENDOOB(以发送带区外数据)。对于 PRU_SENDOOB,发送缓冲区大小可以超过 512 字节,将释放任何分配的内存并中断控制。否则,sbappend() 和 tcp_output() 函数由 PRU_SEND 和 PRU_SENDOOB 调用。
sbappend() 在发送缓冲区的末尾添加数据,并且 tcp_output() 将该段发送到接口。

recvmsg

recvmsg(struct proc *p, struct recvmsg_args *uap , int *retval);
struct recvmsg_args
{
 int s,
 struct msghdr *msg,
 int flags,
};

在receive系统调用中:
s是套接字描述符。
msg是指向msghdr结构的指针。
flags指定控制信息

有四个系统调用可用于从连接接收数据:read、readv、recvfrom和recvmsg。虽然recv(进程使用的库函数)、recvfrom和recvmsg只对套接字描述符进行操作,但read和readv可以对任何类型的描述符进行操作。所有读取系统调用最终都会调用soreceive()。
Socket 系统调用深入研究(TCP协议的整个通信过程)_第9张图片
上图展示了recv的系统调用流程,recvmsg()和recvit()函数初始化各种数组和结构,以将接收到的数据从内核发送到进程。recvit()调用soreceive(),它将接收到的数据从套接字缓冲区传输到接收缓冲区进程。soreceive函数的作用是执行各种检查,例如:
(1)是否设置了MSG_OOB标志。
(2)进程是否正在尝试接收数据。
(3)是否应该在足够的数据到达之前阻塞。
(4)将读取的数据传输到进程。
(5)检查数据是否为带外数据或常规数据,并进行相应处理。
(6)数据接收完成时通知协议。

当设置MSG_OOB标志或数据接收完成时,soreceive()函数会发出依赖于协议的请求。在接收带外数据的情况下,协议层检查不同的条件,以验证接收到的数据是OOB,然后将其返回到套接字层。在后一种情况下,协议层调用tcp_output(),将窗口更新段发送到网络。这会通知另一端任何可用于接收数据的空间。

TCP 四次挥手

当服务器与客户端不在需要进行数据的收发时,可以使用close编程接口关闭socket,socket关闭需要经过四次挥手过程。
Socket 系统调用深入研究(TCP协议的整个通信过程)_第10张图片
偷懒一下:四次挥手过程:TCP四次挥手过程

Close

soo_close(struct file ∗fp , struct proc ∗p);

fp是指向文件结构的指针。
p是指向调用进程的proc结构的指针。

close系统调用关闭或中止套接字上任何挂起的连接。
soo_close()只调用so_close()函数,该函数首先检查要关闭的套接字是否是侦听套接字(接受传入连接的套接字)。如果是,则遍历两个套接字队列以检查是否存在任何挂起的连接。对于每个挂起的连接,都会调用soabort(),它会发出带有PRU_ABORT的tcp_usrreq()请求,可选的系统调用tcp_drop()会检查套接字的状态。
如果状态是 SYN_RCVD,则通过将状态设置为 CLOSED 并调用 tcp_output() 发送 RST 段。tcp_close() 函数然后关闭套接字。tcp_close 函数更新路由度量结构的三个变量,然后释放套接字持有的资源。
如果状态是 SYN_RCVD,则通过将状态设置为 CLOSED 并调用 tcp_output() 发送 RST 段。tcp_close() 函数然后关闭套接字。tcp_close 函数更新路由度量结构的三个变量,然后释放套接字持有的资源。

如果套接字不是侦听套接字,则控制开始使用 soclose(),以检查是否已存在附加到套接字的控制块。如果不存在,则 sofree() 释放套接字。如果存在,则调用具有 PRU_DETACH 的 tcp_usrreq() 将协议与套接字分离。PRU_DETACH 的切换实例调用 tcp_disconnect(),以检查连接状态是否为 ESTABLISHED。如果不是,则 tcp_disconnect() 调用 tcp_close(),以释放 Internet 控制块。否则,tcp_disconnect() 检查延迟时间和延迟套接字选项。如果设置了该选项,并且延迟时间为零,则调用 tcp_drop()。如果未设置,则调用 tcp_usrclosed(),以设置套接字的状态,并调用 tcp_output()(如果需要发送 FIN 段)。
close系统调用如下:
Socket 系统调用深入研究(TCP协议的整个通信过程)_第11张图片
涉及到挥手过程的状态的一些问题

fin_wait1和last_ack状态不会存在太久,因为如果另一方没有发送ack回应时,到了一定时间会超时重传,但是fin_wait_2的时间可能会存在很长的时间,这是为什么呢?
出现这种问题的可能原因是服务器没有close(可能在调用close之前的业务比较耗时,这时候可以将这些业务放到其他线程或者将close放在业务之前),导致fin_wait_2会等待较长时间,这时候的解决方法可以设置一定的超时时间,如果没有收到服务器的FIN,客户端可以直接关闭进程.

为什么会出现CLOSING状态
CLOSING:这个状态是一个比较特殊的状态,也比较少见,正常情况下不会出现,但是当双方同时都作为主动的一方。
Socket 系统调用深入研究(TCP协议的整个通信过程)_第12张图片

常常涉及到面试题

  1. TCP三次握手过程?或者为什么需要3次握手?
  2. TCP四次挥手过程?或者问为什么需要四次挥手?
  3. TIME_WAIT状态持续时间及原因
  4. 超时重传和快速重传
  5. TCP首部长度,有哪些字段
  6. TCP在listen时的参数backlog的意义?
  7. Accept发生在三次握手的哪一步?
  8. 三次握手过程中有哪些不安全性?
  9. TCP与UDP的区别?

你可能感兴趣的:(零声-linux课程总结,tcp/ip,socket,网络编程,socket,系统调用,TCP通信)