Linux编程
2021-08-19 16:39:17 0 举报
AI智能生成
linux系统编程 linux网络编程 linuxC/C++网络编程
作者其他创作
大纲/内容
I/O
目录
进程
查看进程状态
ps-eopid,ppid,sid,tty,pgrp,comm,stat|grep-E'bash|PID|进程名'
进程状态
D 不可中断的休眠状态(通常是I/O的进程),可以处理信号,有延迟<br>R 可执行状态&运行状态(在运行队列里的状态)<br>S 可中断的休眠状态之中(等待某事件完成),可以处理信号<br>T 停止或被追踪(被作业控制信号所停止)<br>Z 僵尸进程<br>X 死掉的进程<br>< 高优先级的进程<br>N 低优先级的进程<br>L 有些页被锁进内存<br>s Session leader(进程的领导者),在它下面有子进程<br>t 追踪期间被调试器所停止<br>+ 位于前台的进程组<br>
strace 进程跟踪工具
跟踪进程 pid 所收到的信号<br>sudostrace-etrace=signal-p pid<br>
进程标识
每个进程都有一个非负整型的唯一进程ID
进程ID
pid_t getpid()
父进程ID
pid_t getppid()
获取回话进程ID
pid_t getsid(pid_t pid) // 获取pid的回话进程ID
设置回话进程ID
pid_t setsid() // 设置当前进程在新的session中
创建子进程<br>pid_t fork()<br>
现有进程创建子进程的唯一方法,
创建的子进程开始执行fork()后面与父进程相同的代码
fork 产生的子进程并不复制父进程的内存空间,而是和父进程一起共享一个内存空间,但这个内存空间修改时,那么这个内存就会复制一份给该进程单独使用,以免影响到共享这个内存空间的其他进程
fork函数返回两次,父进程中返回一次,子进程中返回一次;>0 : 父进程; ==0 子进程; <0 : 失败
僵尸进程
当子进程比父进程先结束,而父进程又没有(调用wait/waitpid)回收子进程,系统(init进程)释放子进程占用的资源不完全(没法释放子进程占用的pid资源),此时子进程将成为一个僵尸进程。
解决僵尸进程
SIGCHLD信号处理:处理子进程终止后发送的SIGCHLD信号
1. 父进程中忽略子进程 SIGCHLD信号,有内核回收
signal(SIGCHLD,SIG_IGN)
2. 父进程调用wait/waitpid 等待子进程结束。wait阻塞,waitpid 可以传递WNOHANG 使父进程不阻塞立即返回。
3. 父进程注册SIGCHLD函数,调用wait/waitpid
pid_t wait(int *status)
阻塞,自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止,参数status用来保存被收集进程退出时的一些状态
pid_t waitpid(pid_t pid int *status, int options)
options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起.<br><br>pid==-1等待任一子进程。于是在这一功能方面waitpid与wait等效。<br>pid>0等待其进程ID与pid相等的子进程。<br>pid==0等待其组ID等于调用进程的组ID的任一子进程。换句话说是与调用者进程同在一个组的进程。<br>pid<-1等待其组ID等于pid的绝对值的任一子进程<br><br><br>
守护进程
普通进程
ps-eopid,ppid,sid,tty,pgrp,comm,stat,cmd|grep-E'bash|PID|进程名'
进程有对应的终端,终端退出,那么进程也就消失了。
父进程是一个bash
1. 没有控制它的终端(tty),在后台运行
3. 必须是一个session leader
4. 必须是一个进程组的leader
5. root 目录即为工作目录
6. umask 设置为0
源码实例
进程间同步
线程
线程同步
信号
在程序中信号提供一种处理一步事件的方法;
信号名称和编号
以“SIG”开头,定义在signal.h 头文件中,信号都是大于0的正整数
kill -l #查看所有信号
man 7 signal #来查看系统定义
产生信号
按键产生
ctrl+c、ctrl+z、ctrl+\ 等
系统调用产生
kill
int kill(pid_t pid,int sig)<br>pid >0 : 发送信号给指定进程<br>pid = 0: 发送信号给调用者同组的所有进程<br>pid <0: 取|pid|发给对应进程组<br>成功返回0,失败返回-1<br>
raise
# 给当前进程发送指定信号(自己发给自己)<br>int raise(int sig);<br><br>raise(signo)==kill(getpid(),signo);<br>
abort
给自己发送异常终止信号6)SIGABRT信号,<br>void abort(void);无返回<br>
软件条件产生
定时器 alarm
在指定seconds后,内核会给当前进程发送14)SIGALRM信号。<br>进程收到该信号,默认动作终止。每个进程都有且只有唯一个定时器<br><br>unsigned int alarm(unsigned int seconds); 返回剩余秒数<br>
alarm(0) 取消定时
设置定时器,可替代alarm<br>int setitimer(int which,const struct itimerval *new_value,struct itimerval* old_value);<br><br>which: <br> 自然定时 ITIMER_REAL -> SIGLARM 14 <br> 用户空间计时 ITIMER_VIRTUAL -> SIGVTALRM 26<br> 运行时计时 ITIMER_PROF -> SIGPROF 27
硬件异常产生
非法内存访问、除0、内存对齐错误等
命令产生
kill
kill -SIGKILL pid
信号处理方式
信号默认动作
A终止进程<br>B忽略信号<br>C进程终止时,会在进程的当前工作目录生产一个core文件,该文件是进程终止时的内存快照,以便以后供debugger调试用。<br>以下情况不会生产core文件:<br>(1)为程序设置了set-user-ID并且用户不是程序的所有者;<br>(2)为程序设置了set-group-ID并且用户不是程序的组所有者;<br>(3)进程在当前工作目录下面没有写权限;<br>(4)当前工作目录下已有core文件且进程对该core文件没有写权限;<br>(5)core文件过大。<br>D停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)<br>E信号不能被捕获<br>F信号不能被忽略<br>G进程继续(曾被停止的进程)<br>
忽略
大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程。<br>
#include<signal.h><br>...<br>signal(SIGINT,SIG_IGN); // 表示忽略信号SIGINT(SIGKILL、SIGSTOP 不能忽略)
信号屏蔽
信号集 sigset_t
每个进程中默认都有一个信号集 sigset;它决定了进程自动屏蔽哪些信号;当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由 sigset 来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为sigset。<br>
XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
信号屏蔽操作
sigset_t set;//typedef unsigned long sigset_t;<br><br>int sigemptyset(sigset_t *set); 将某个信号集清0成功:0;失败:-1<br>int sigfillset(sigset_t *set); 将某个信号集置1成功:0;失败:-1<br>int sigaddset(sigset_t *set,int signum); 将某个信号加入信号集成功:0;失败:-1<br>int sigdelset(sigset_t *set,int signum); 将某个信号清出信号集成功:0;失败:-1<br>int sigismember(const sigset_t *set,int signum); 判断某个信号是否在信号集中返回值:在集合:1;不在:0;出错:-1<br>
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset); 设置信号集到当前进程;成功:0;失败:-1,设置errno<br>how参数取值:假设当前的信号屏蔽字为mask<br> 1.SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask=mask|set<br> 2.SIG_UNBLOCK:当how设置为此,set表示需要解除屏蔽的信号。相当于mask=mask&~set<br> 3.SIG_SETMASK:当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask=set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达<br>
int sigpending(sigset_t *set); 读取当前进程的未决信号集;set传出参数。返回值:成功:0;失败:-1,设置errno<br><br>一个已经产生的信号,但是还没有传递给任何进程,此时该信号的状态就称为未决状态。<br>
捕捉
说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
信号捕捉处理函数
signal
typedef void(*sighandler_t)(int);<br><br># 注册信号signum 到函数 handler, 传SIG_IGN表示忽略<br>sighandler_t signal(int signum,sighandler_t handler); <br>
sigaction
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);<br><br>参数:<br> act:传入参数,新的处理方式。<br> oldact:传出参数,旧的处理方式。<br> 成功:0;失败:-1,设置errno<br>
#include<stdio.h><br>#include<string.h><br>#include<unistd.h><br>#include<stdlib.h><br>#include<signal.h><br><br>void fun(int sig)<br>{<br> printf("%d\n",sig);<br>}<br><br>intmain()<br>{<br> struct sigaction action;<br> action.sa_handler=fun;<br> sigemptyset(&action.sa_mask);<br> action.sa_flags=0;<br> sigaction(SIGINT,&action,NULL);<br> while(1);<br>}<br>
struct sigaction{<br> void(*sa_handler)(int);<br> void(*sa_sigaction)(int,siginfo_t*,void*);<br> sigset_tsa_mask;<br> intsa_flags;<br> void(*sa_restorer)(void);<br>};<br>
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)<br>sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)<br><br>sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作<br>sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。<br>sa_flags:通常设置为0,表使用默认属性。<br>
可/不可重入函数
一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。
显然,insert函数是不可重入函数,重入调用,会导致意外结果呈现。究其原因,是该函数内部实现使用了全局变量
1.定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free
2.信号捕捉函数应设计为可重入函数
3.信号处理程序可以调用的可重入函数可参阅man7signal
4.没有包含在上述列表中的函数大多是不可重入的,其原因为:
a)使用静态数据结构
b)调用了malloc或free
c)是标准I/O函数
网络
netstat 显示网络相关信息
# 查看 9000端口状态<br>netstat -anp | grep 'State|9000' <br>
TCP
协议简述
TCP 提供面向有连接的通信传输,面向有连接是指在传送数据之前必须先建立连接,数据传送完成后要释放连接。
无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。
同时由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式,所以需要四次挥手关闭连接。
TCP数据包的封装
数据传输过程
TCP包封装
<b>TCP端口号</b><br> TCP的连接是需要四个要素确定唯一一个连接:<br> (源IP,源端口号)+ (目地IP,目的端口号)<br> 所以TCP首部预留了两个16位作为端口号的存储,而IP地址由上一层IP协议负责传递源端口号和目地端口各占16位两个字节,也就是端口的范围是2^16=65535;另外1024以下是系统保留的,从1024-65535是用户使用的端口范围<br><br><b>TCP的序号和确认号:</b><br> 1. 32位序号 seq:Sequence number 缩写seq ,TCP通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,比如现在序列号为1000,发送了1000,下一个序列号就是2000。<br> 2. 32位确认号 ack:Acknowledge number 缩写ack,TCP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。<br><br><b>TCP的标志位</b><br> 每个TCP段都有一个目的,这是借助于TCP标志位选项来确定的,允许发送方或接收方指定哪些标志应该被使用,以便段被另一端正确处理。<br>用的最广泛的标志是 SYN,ACK 和 FIN,用于建立连接,确认成功的段传输,最后终止连接。<br><br><ol><li><span style="font-size: inherit;">SYN:简写为S,同步标志位,用于建立会话连接,同步序列号;</span></li><li><span style="font-size: inherit;">ACK: 简写为.,确认标志位,对已接收的数据包进行确认;</span></li><li><span style="font-size: inherit;">FIN: 简写为F,完成标志位,表示我已经没有数据要发送了,即将关闭连接;</span></li><li><span style="font-size: inherit;">PSH:简写为P,推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队;</span></li><li><span style="font-size: inherit;">RST:简写为R,重置标志位,用于连接复位、拒绝错误和非法的数据包;</span></li><li><span style="font-size: inherit;">URG:简写为U,紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;</span></li></ol>
TCP三次握手建立
所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个报文。<br>三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。<br>
<b>第一次握手:</b><br><ul><li>客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。</li></ul><br><b>第二次握手:</b><br><ul><li>服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。</li></ul><br><b>第三次握手:</b><br><ul><li>客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。</li></ul>
抓包实例分析
<b>第一次握手:</b><br>fce0 : 64736 (16位源端口)<br>1f40 : 8000 (16位目的端口)<br>cf249414 : 3475280916 (32位序号)<br>00000000 :0 (32位确认号)<br>8 : 8 (4位头部长度)<br>002 :000000000010 (6位保留和6位标识 'SYN')<br>ffff : 65535 16位窗口大小<br>ba3e :16位校验和<br>0000 :16位紧急指针<br><br>02 04 ff d7 01 03 03 08 01 01 04 02 :IP包数据<br>
<b>第二次握手:</b><br><ol style=""><li style=""><span style="font-size: inherit;">由服务端端口到客户端端口</span></li><li style=""><span style="font-size: inherit;">服务器端将TCP报文标志位 SYN和ACK都置为 1</span></li><li style=""><span style="font-size: inherit;">生成服务端序号 590af0e8</span></li><li style=""><span style="font-size: inherit;">32位确认号 是第一次握手的32位序号+1</span></li></ol>
<b>第三次握手:</b><br><ol><li>32位序号为第二次握手的32位确认号(第一次握手的序号+1)</li><li>32位确认号 为 第二次握手的 32位序号+1</li><li>ACK 置为 1</li></ol>
为什么需要三次握手
我们假设client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。<br><br>本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。<br><br>假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。<br><br>所以,采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。<br><br>TCP 三次握手跟现实生活中的人与人打电话是很类似的:<br><br>三次握手:<br>“喂,你听得到吗?”<br>“我听得到呀,你听得到我吗?”<br>“我能听到你,今天 balabala……”<br><br>经过三次的互相确认,大家就会认为对方对听的到自己说话,并且愿意下一步沟通,否则,对话就不一定能正常下去了。
TCP四次挥手关闭连接
挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起:<br><br>第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。<br>第二次分手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。<br>第三次分手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。<br>第四次分手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。
为什么连接的时候是三次握手,关闭的时候却是四次握手?
建立连接时因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。所以建立连接只需要三次握手。<br><br>由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式。<br>这就意味着,关闭连接时,当Client端发出FIN报文段时,只是表示Client端告诉Server端数据已经发送完毕了。当Server端收到FIN报文并返回ACK报文段,表示它已经知道Client端没有数据发送了,但是Server端还是可以发送数据到Client端的,所以Server很可能并不会立即关闭SOCKET,直到Server端把数据也发送完毕。<br>当Server端也发送了FIN报文段时,这个时候就表示Server端也没有数据要发送了,就会告诉Client端,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
为什么要等待2MSL?
MSL:报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。<br>有以下两个原因:<br><br>第一点:保证TCP协议的全双工连接能够可靠关闭:<br>由于IP协议的不可靠性或者是其它网络原因,导致了Server端没有收到Client端的ACK报文,那么Server端就会在超时之后重新发送FIN,如果此时Client端的连接已经关闭处于CLOESD状态,那么重发的FIN就找不到对应的连接了,从而导致连接错乱,所以,Client端发送完最后的ACK不能直接进入CLOSED状态,而要保持TIME_WAIT,当再次收到FIN的收,能够保证对方收到ACK,最后正确关闭连接。<br>第二点:保证这次连接的重复数据段从网络中消失<br>如果Client端发送最后的ACK直接进入CLOSED状态,然后又再向Server端发起一个新连接,这时不能保证新连接的与刚关闭的连接的端口号是不同的,也就是新连接和老连接的端口号可能一样了,那么就可能出现问题:如果前一次的连接某些数据滞留在网络中,这些延迟数据在建立新连接后到达Client端,由于新老连接的端口号和IP都一样,TCP协议就认为延迟数据是属于新连接的,新连接就会接收到脏数据,这样就会导致数据包混乱。所以TCP连接需要在TIME_WAIT状态等待2倍MSL,才能保证本次连接的所有数据在网络中消失。
TCP状态
11 种状态
LISTEN:等待从任何远端TCP 和端口的连接请求。<br><br>SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。<br><br>SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。<br><br>ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。<br><br>FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。<br><br>FIN_WAIT_2:等待远端TCP 的连接终止请求。<br><br>CLOSE_WAIT:等待本地用户的连接终止请求。<br><br>CLOSING:等待远端TCP 的连接终止请求确认。<br><br>LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)<br><br>TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。<br>TIME_WAIT 两个存在的理由:<br> 1.可靠的实现tcp全双工连接的终止;<br> 2.允许老的重复分节在网络中消逝。<br><br>CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
监听套接字队列
int listen(int sockfd, int backlog);
调用listen()进行监听的套接字,操作系统会给这个套接字 维护两个队列;
a. 未完成连接队列
当客户端 发送tcp连接给服务器时,服务器创建此队列。
b. 已完成连接队列
完成三次握手后,把未完成队列中的socket移动到此队列。
backlog
原含义 :已完成队列和未完成队列里边条目之和 不能超过 backlog;
新含义 : 已完成队列的条目
RTT
客户端,这个RTT时间是第一次和第二次握手加起来的时间;
服务器,这个RTT时间实际上是第二次和第三次握手加起来的时间
阻塞与非阻塞
调用函数获取数据,系统没有满足条件的数据时,进程进入休眠,默认的socket 是阻塞的。
调用函数获取数据,系统无满足条件的数据时,返回错误标识;
int fcntl(int fd, int cmd, ... /* arg */ );<br><br>fd : IO<br>cmd : F_GETFL(获取IO标识)和F_SETFL(设置文件标志)<br><br><ul><li><span style="font-size: inherit;">O_NONBLOCK 非阻塞I/O;如果read(2)调用没有可读取的数据,或者如果write(2)操作将阻塞,read或write调用返回-1和EAGAIN错误 </span></li><li><span style="font-size: inherit;">O_APPEND 强制每次写(write)操作都添加在文件大的末尾,相当于open(2)的O_APPEND标志 </span></li><li><span style="font-size: inherit;"> O_DIRECT 最小化或去掉reading和writing的缓存影响.系统将企图避免缓存你的读或写的数据. 如果不能够避免缓存,那么它将最小化已经被缓存了的数 据造成的影响.如果这个标志用的不够好,将大大的降低性能 </span></li><li> O_ASYNC 当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候</li></ul>
/* 设置 socket 属性为非阻塞方式 */<br>if(fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {<br> perror("fcntl");<br> exit(errno);<br>}
同步IO与异步IO
异步I/O:<br><ul><li>调用一个异步I/O函数时,我门要给这个函数指定一个接收缓冲区,我还要给定一个回调函数;调用完一个异步I/O函数后,该函数会立即返回。 其余判断交给操作系统,操作系统会判断数据是否到来,如果数据到来了,操作系统会把数据拷贝到你所提供的缓冲区里,然后调用你所指定的这个回调函数来通知你;</li></ul>
同步I/O:<br><ul><li>select/poll : </li></ul><span style="font-size: inherit;"> 调用select()判断有没有数据,有数据返回,无数据阻塞<br></span> select()返回之后,用recvfrom()去取数据
I/O复用:<br>用select这种同步I/O函数处理多个连接,只要其中一个连接有数据到来,就返回,用recvfrom 去去数据<br>
<ol><li>建立连接之前服务器和客户端的状态都为 CLOSED;</li><li>服务器创建 Socket后开始监听, 服务端变为 LISTEN 监听状态;</li><li>客户端请求建立连接,向服务器发送 SYN 报文,客户端状态变为 SYN_SENT;</li><li>服务器收到客户端的报文后向客户端发送 ACK和SYN报文,此时服务器状态变为 SYN_RCVD;</li><li>客户端收到ACK 、SYN后,就向服务端发送ACK,客户端状态变为 ESTABLISHED;</li><li>服务器收到客户端的ACK后 变为 ESTABLISHED,状态,此时3次握手完成。</li></ol>
<ol><li>客户端先向服务器发送FIN报文,请求断开连接,其状态变为FIN_WAIT1;</li><li>服务器收到FIN后向客户端发送ACK,服务器的状态围边CLOSE_WAIT;</li><li>客户端收到ACK后就进入FIN_WAIT2状态,此时连接已经断开了一半了。如果服务器还有数据要发送给客户端,就会继续发送;</li><li>直到发完数据,就会发送FIN报文,此时服务器进入LAST_ACK状态;</li><li>客户端收到服务器的FIN后,马上发送ACK给服务器,此时客户端进入TIME_WAIT状态;</li><li>再过了2MSL长的时间后进入CLOSED状态。服务器收到客户端的ACK就进入CLOSED状态。</li><li>至此,还有一个状态没有出来:CLOSING状态。<br><br></li></ol>CLOSING状态表示:<br>客户端发送了FIN,但是没有收到服务器的ACK,却收到了服务器的FIN,这种情况发生在服务器发送的ACK丢包的时候,因为网络传输有时会有意外。<br>
epoll
epoll原理
int epoll_create(int size);
创建一个代表该 Epoll 的 eventpoll 对象,
rbr : 监视列表(红黑树)<br>rdlist:就绪列表(引用收到数据的Socket)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
维护监视列表(增加、删除、修改 Socket)
op 参数:<br><br> EPOLL_CTL_ADD:向interest list添加一个需要监视的描述符<br> EPOLL_CTL_DEL:从interest list中删除一个描述符<br> EPOLL_CTL_MOD:修改interest list中一个描述符<br>
event 参数:
epoll的两种触发方式
水平触发的时机
对于读操作,只要缓冲内容不为空,LT模式返回读就绪。
对于写操作,只要缓冲区还不满,LT模式会返回写就绪。
边缘触发的时机
当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
当有新数据到达时,即缓冲区中的待读数据变多的时候。
当缓冲区有数据可读,且应用进程对相应的描述符进行
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。
events: 用来记录被触发的events,其大小应该和maxevents一致
maxevents: 返回的events的最大个数
timeout 参数:<br> timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;<br> timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;<br> timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时<br>
工作流程
调用 epoll_create 创建一个eventpoll对象(维护一个监控列表和就绪列表)
epoll_ctl 维护监控空列表
接收数据
Socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 Socket 引用
阻塞和唤醒进程
程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程
当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态
epoll设计思想
一、功能分离
Select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一
每次调用 Select 都需要这两步操作,然而大多数应用场景中,需要监视的 Socket 相对固定,并不需要每次都修改<br>Epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升<br>
先用 epoll_create 创建一个 Epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据
二、就绪列表
Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。
计算机共有三个 Socket,收到数据的 Sock2 和 Sock3 被就绪列表 Rdlist 所引用。<br>当进程被唤醒后,只要获取 Rdlist 的内容,就能够知道哪些 Socket 收到数据
Select设计思想
假如能够预先传入一个 Socket 列表,如果列表中的 Socket 都没有数据,挂起进程,直到有一个 Socket 收到数据,唤醒进程。
1. 准备一个数组 FDS,让 FDS 存放着所有需要监视的 Socket。<br>2. 调用 Select,如果 FDS 中的所有 Socket 都没有数据,Select 会阻塞,直到有一个 Socket 接收到数据,Select 返回,唤醒进程。<br>3. 用户可以遍历 FDS,通过 FD_ISSET 判断具体哪个 Socket 收到数据,然后做出处理。<br>
Select 的实现思路很直接,假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中。<br>操作系统把进程 A 分别加入这三个 Socket 的等待队列中。<br>当任何一个 Socket 收到数据后,中断程序将唤起进程。下图展示了 Sock2 接收到了数据的处理流程:<br>
Sock2 接收到了数据,中断程序唤起进程 A。<br>所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面<br><br>注: 当程序调用 Select 时,内核会先遍历一遍 Socket,如果有一个以上的 Socket 接收缓冲区有数据,那么 Select 直接返回,不会阻塞。
将进程 A 从所有等待队列中移除,再加入到工作队列里面<br>经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。<br>
这种简单方式行之有效,在几乎所有操作系统都有对应的实现。但是简单的方法往往有缺点,主要是:
1. 每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。
2. 进程被唤醒后,程序并不知道哪些 Socket 收到数据,还需要遍历一次。
服务端需要管理多个客户端连接,而 Recv 只能监视单个 Socket,这种矛盾下,人们开始寻找监视多个 Socket 的方法;
工作队列
计算机中运行着 A、B 与 C 三个进程,其中进程 A 执行着上述基础网络程序,一开始,这 3 个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。
当进程 A 执行到创建 Socket 的语句时,操作系统会创建一个由文件系统管理的 Socket 对象(如下图)
这个 Socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该 Socket 事件的进程。
当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中
由于工作队列只剩下了进程 B 和 C,依据进程调度,CPU 会轮流执行这两个进程的程序,不会执行进程 A 的程序。所以进程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源。<br>注:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。<br>
当 Socket 接收到数据后,操作系统将该 Socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。<br>同时由于 Socket 的接收缓冲区已经有了数据,Recv 可以返回接收到的数据。
唤醒队列
首先从硬件分析
1. 网卡接收到网线传来的数据<br>2. 通过硬件DMA、IO传输到内存中<br>3. 内存保存数据到缓冲区<br>4. 网卡传输完成后,向cpu发送一个中断信号,cpu就知道了信数据到来,并处理
0 条评论
下一页