嵌入式详解之系统调用
Linux开发架构之路 2024-06-07

内核态和用户态

通俗的说,用户空间就是运行着用户编写的应用程序的虚拟内存空间。在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)。

词汇解释

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

通信五元组

服务器与客户端的通信是通过五元组来区分的,五元组的元素通常是指源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的操作实际上也是对某种特殊文件的读写操作。只不过操作的文件在服务器和客户端各自拥有一个。

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

内核操作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系统调用

上图显示了 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函数就会调用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系统调用的流程如下:

如果指定的本地端口为非零值,则如果绑定位于保留端口上(例如,根据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系统调用流程如下。

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系统调用流程如下:

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系统调用过程如下:

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种想法都是错误的。

上图简单的表示了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系统调用流程如下:

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()。

上图展示了recv的系统调用流程,recvmsg()和recvit()函数初始化各种数组和结构,以将接收到的数据从内核发送到进程。recvit()调用soreceive(),它将接收到的数据从套接字缓冲区传输到接收缓冲区进程。soreceive函数的作用是执行各种检查,例如:

(1)是否设置了MSG_OOB标志。

(2)进程是否正在尝试接收数据。

(3)是否应该在足够的数据到达之前阻塞。

(4)将读取的数据传输到进程。

(5)检查数据是否为带外数据或常规数据,并进行相应处理。

(6)数据接收完成时通知协议。

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

TCP 四次挥手

当服务器与客户端不在需要进行数据的收发时,可以使用close编程接口关闭socket,socket关闭需要经过四次挥手过程。

四次挥手过程这里不再描述.

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系统调用如下:

涉及到挥手过程的状态的一些问题

fin_wait1和last_ack状态不会存在太久,因为如果另一方没有发送ack回应时,到了一定时间会超时重传,但是fin_wait_2的时间可能会存在很长的时间,这是为什么呢?

出现这种问题的可能原因是服务器没有close(可能在调用close之前的业务比较耗时,这时候可以将这些业务放到其他线程或者将close放在业务之前),导致fin_wait_2会等待较长时间,这时候的解决方法可以设置一定的超时时间,如果没有收到服务器的FIN,客户端可以直接关闭进程.

为什么会出现CLOSING状态

CLOSING:这个状态是一个比较特殊的状态,也比较少见,正常情况下不会出现,但是当双方同时都作为主动的一方。



声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
  • 相关技术文库
  • 单片机
  • 嵌入式
  • MCU
  • STM
  • 单片机和芯片的区别对于嵌入式系统设计有何影响?

    单片机的使用非常广泛,可以说,单片机就是一个微型的计算机。为增进大家对单片机的认识,小编在本文中将对51单片机的CPU以及51单片机的内容结构进行详细介绍。如果你对单片机具有兴趣,不妨和小编一起继续往下阅读...

    3小时前
  • 单片机的执行速度是否受到编程语言的影响?

    单片机可以说是一个微型计算机系统,通过单片机,能够创造出很多有意思的小玩意。为增进大家对单片机的认识,本文将对单片机的工作条件以及51单片机和52单片机的区别予以介绍。如果你对单片机具有兴趣,不妨继续往...

    3小时前
  • 单片机的未来特性多样化:如何使用单片机进行物联网开发?

    单片机用户(原始设备制造商)面临着三大挑战:通过特性、性能或价格实现终端产品差异化;通过缩短产品上市时间以补偿在复杂设计上日益增长的投资;力求在不增加成本的前提下达成上述两大目标。这些挑战构成了未来单片...

    3小时前
  • Linux系统内置模块参数的查看

    提问:我想要知道Linux系统中内核内置的模块,以及每个模块有哪些参数。有什么方法可以得到内置模块和设备驱动的列表,以及它们的详细信息呢? 现代Linux内核正在随着时间变化而迅速增长,以支持大量的硬件、文件系 ... 评论:1 分享:0 收藏:4      2015-11-02 08:00      Dan Nanni, geekpi

    3小时前
  • 在 Linux 上安装 screenfetch

    想在屏幕上显示出你的 Linux 发行版的酷炫标志和基本硬件信息吗?不用找了,来试试超赞的 screenfetch 和 linux_logo 工具。 来看看 screenfetch 吧 screenFetch 是一个能够在截屏中显示系统/主题信息的命令行脚本 ... 评论:7 分享:0 收藏:5      2015-11-02 09:52      Vivek Gite, alim0x

    3小时前
  • 单片机的工作原理

    一、单片机内部结构分析我们来思考一个问题,当我们在编程器中把一条指令写进单片机内部,然后取下单片机,单片机就

    3小时前
  • 存储设备SCSI接口标准

    SCSI协议介绍SCSI,全称Small Computer System Interface,即小型计算机接口

    8小时前
  • 详细说说车规级汽车MCU

    控制类芯片主要就是指MCU(Microcontroller Unit),即微控制器,又叫单片机,是把CPU的主频与规格做适当缩减,并将存储器、定时器、A/D转换、时钟、I/O端口及串行通讯等多种功能模块和接口集成在单个芯片上。

    昨天
  • 把GuiLite移植到STM32上

    STM32单片机上流畅运行

    昨天
  • SRAM与DRAM有何不同?一文带你轻松搞懂!

    在半导体存储器的发展中,静态存储器(SRAM)由于其广泛的应用成为其中不可或缺的重要一员。 随

    06-14
  • 描述linux io_uring 性能

    先看看性能io_uring 需要内核版本在5.1 及以上才支持,liburing的编译安装 很简单,直接clo

    06-14
  • 工程师对单片机编程的总结

    ller Unit 的简称,中文叫微控制器,俗称单片机,是把CPU的频率与规格做适当缩减,并将内存、计数器、USB、A/D转换、UART、PLC、DMA等周边接口。

    06-14
下载排行榜
更多
评测报告
更多
EE直播间
更多
广告