linux的五种I/O模型、socket知识总结

1. Linux中的5种I/O模型
  • read操作为例,I/O需要经历两个阶段:
  1. 等待数据准备好(waiting for the data to be eady
  2. 将数据从内核拷贝到进程中(copying the data from the kernel to the process
  • 如果是socket中的read操作,第一步通常等待数据从网络中到达,数据到达后会被复制到内核的接收缓冲区中;第二步,将内核接收缓冲区中的数据复制到应用进程缓冲区
  • 根据这两个阶段的不同表现,Linux中有5种I/O模型:
  1. 同步模式: 阻塞式I/O(BIO)、非阻塞式I/O(NIO)、I/O多路复用、信号驱动式I/O(SIGIO),共四种
  2. 异步模式: 只有异步I/O(AIO
    在这里插入图片描述
① 阻塞式I/O(BIO)
  • 阻塞式I/O: 应用进程执行系统调用后被阻塞,直到数据准备好并从内核缓冲区复制到应用进程缓冲区才返回。
  • 以socket的接收操作为例:
  1. 应用进程调用recvfrom()函数接收对方socket传来的数据, recvfrom()函数可以产生一个系统调用
  2. 内核收到这个系统调用后先等待数据准备好,然后将数据从内核复制到应用进程中并返回OK
  3. 应用进程在这个过程中会阻塞,直到收到系统调用返回的OK后,才能继续执行后续代码。
    在这里插入图片描述
  • recvfrom()函数:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
	struct sockaddr *src_addr, socklen_t *addrlen);
  • socketpipeFIFOterminalI/O默认模式都是阻塞式I/O。
  • 阻塞式I/O的优点: 应用进程被阻塞时会放弃CPU时间,让其他进程执行。这样不会浪费CPU时间,使得CPU的利用率比较高
② 非阻塞式I/O(NIO)
  • 非阻塞式I/O: 应用进程执行系统调用后会立马收到返回值,有可能是错误码(errno),也有可能是OK。
  • 虽然应用进程未被阻塞,但为了保证I/O操作的成功,应用进程需要不断主动的查询结果,即需要进行轮询(polling)。
  • 以socket的接收操作为例:
  1. 应用进程调用recvfrom()函数接收对方socket发来的数据,recvfrom()函数会产生系统调用。
  2. 如果数据未准备好,系统调用直接返回EWOULDLOCK或者EAGAIN错误码。
  3. 应用进程会进行轮询,直到某次系统调用达到时,数据已经准备好,内核将数据复制到应用进程缓冲区并返回OK
  4. 应用进程收到系统调用返回的OK后,可以停止轮询,继续执行其他代码。
    在这里插入图片描述
  • 非阻塞式I/O的缺点: 由于应用进程使用轮询的方式确保I/O操作的成功,它会占用CPU时间导致CPU利用率不高
③ I/O多路复用
  • I/O多路复用:
  1. 首先,应用进程调用select()或者poll()或者epoll()函数监听多个socket是否可读(数据是否准备好)。
  2. 这一过程应用进程会被阻塞,直到某个socket变得可读。
  3. 接着,应用进程调用**recvfrom()**发起系统调用,然后内核将数据复制到应用进程中并返回OK
    在这里插入图片描述
  • I/O多路复用可以使单个进程具有处理多个I/O事件的能力,又被称为事件驱动I/OEvent Driven I/O)。
  • I/O多路复用的优点: socket连接数量大的系统中,I/O多路复用可以使系统开销更小
  1. 如果一个web服务器没有使用I/O多路复用,那么对于每个socket连接都需要创建一个线程去处理。
  2. 如果同时有几万个socket连接,那么就需要创建相同数目的线程。
  3. 相比于多进程和多线程技术,使用I/O多路复用管理多个socket连接,可以减少进程、线程创建和切换的开销。
④ 信号驱动I/O
  • 信号驱动I/O:
  1. 应用进程通过sigaction系统调用,向系统注册一个处理SIGIO的信号处理程序signal handler)。
  2. 该系统调用会立即返回,然后应用进程可以继续执行。
  3. 当数据准备好后,内核会向signal handler发送SIGIO信号。
  4. signal handler收到该信号后,会调用recvfrom()函数发起系统调用,然后内核将数据复制到应用进程中并返回OK
    在这里插入图片描述
  • 信号驱动I/O的优点:
  1. 信号驱动I/O模型中,应用进程在数据的准备阶段是非阻塞的,在数据的复制阶段是阻塞的,。
  2. 相比于通过轮询的非阻塞I/O,信号驱动I/O的CPU利用率更高
⑤ 异步I/O(AIO)
  • 异步I/O:
  1. 应用进程调用aio_read()函数开启异步读操作,该函数会产生一个系统调用并立即返回。
  2. 应用进程可以继续执行,而不会被阻塞。
  3. 当数据准备就绪并从内核复制到应用进程中后,内核会向应用进程发送一个信号。
    在这里插入图片描述
  • 总结:
  1. 异步I/O中的信号是通知应用进程I/O操作已经完成,信号驱动I/O中的信号是通知应用进程可以开始执行I/O操作
  2. 之前的4种I/O模型都是同步模型,数据的接收都需要自己调用recvfrom()函数将数据从内核复制到应用进程中。
  3. 异步I/O模型无需调用recvfrom()函数,操作系统会自动将数据从内核复制到应用进程中,并通知应用进程I/O操作执行完毕。
⑥ 5种模型的比较
  • 同步模型:
  1. 之前的4种I/O模型都是同步模型,将数据从内核复制到应用进程中时,需要应用进程自己调用recvfrom()函数。
  2. 将数据从内核缓冲区复制到应用进程缓冲区的阶段,调用了recvfrom()函数,应用进程会阻塞。
  3. 同步模型中,不同I/O之间的区别主要在第一阶段。
  • 异步模型:
  1. 只有AIO属于异步模型,将数据从内核复制到应用进程中时,应用进程无需调用recvfrom()函数。操作系统会自动完成数据的复制工作,并在完成后通知应用进程。
  2. 数据从内核缓冲区复制到应用进程缓冲区的阶段,应用进程不会阻塞。
    在这里插入图片描述
2. socket
① socket中的同步
  • 应用进程自己调用recvfrom()函数,将数据从内核缓冲区复制到应用进程缓冲区。
  • 同步有两种情况:
  1. 阻塞: 妈妈让烧水,几岁的时候胆小怕出问题,会一直守着直到水烧开。
  2. 非阻塞: 妈妈让烧水,10来岁的时候迷上了偶像剧,烧上水后回房间看剧,一会出来看一下,没烧开又回去看剧。如此反复查看水是否烧开,直到最后水烧开。
② socket中的异步
  • 应用进程调用aio_read()函数后立即返回,内核完成数据等待、数据复制后后,向应用进程发送信号,通知应用进程数据已经复制完毕。
  • 妈妈让烧水,15岁以后烧水壶变先进了,水烧开后会发出声音。于是烧上水就可以安心的会房间看剧了,直到听到水开的声音,出来将茶叶泡上就好。
③ socket中tcp方式如何通过accpet实现连接?
  • listen()函数:
  1. 第一个参数sockfd:将socket设置为被动模式,指示内核可以接受指向该套接字的连接请求。
  2. 第二个参数backlog:指示内核为该socket排队的最大连接个数,包括已完成连接队列未完成连接队列
  3. 设置成功返回0,失败返回-1
int listen(int sockfd, int backlog);
  • 已连接队列和未连接队列:
  1. 已完成三次握手的连接,即处于ESTABLISHED状态的连接,将进入已完成连接队列。
  2. 未完成三次握手的连接,即处于SYN_RCVD状态的连接,将进入未完成连接队列。
  3. 当应用进程调用accept()函数时,如果已完成连接队列不为空,则该队列中的头节点将返回给应用进程;否则,应用进程将会阻塞,直到已完成队列中有新的连接加入才会被唤醒。
  4. 已完成队列和未完成队列之和不超过backlog。SYN分节到达时,如果队列已满TCP将会忽略该分节等待客户超时重传。之所以这么做,因为这么情况是暂时的,客户在下一次重传时,队列很可能已存在可用空间。
  5. 如果服务器针对上面的情况立即响应RST直接判死刑),客户的connect()函数调用就会立即返回一个错误,强制应用进程处理这种错误。而且客户也无法判断RST是由于队列已满,还是由于连接请求中的端口没有服务器在监听

在这里插入图片描述

  • 不要讲backlog设置为0,因为不同的实现对此有不同的解释。如果只是想让客户连接到监听套接字,直接close监听套接字即可。
  • backlog的设置可以通过#define进行宏定义,方便更改backlog值时,只需要改动一个地方即可;还可以通过读取环境变量的值来设置backlog的值。
④ socket中的tcp三次握手
  • socket中连接的创建、通信、释放,过程示意图如下:
    在这里插入图片描述
  • socket中TCP的三次握手:
  1. 客户调用connect()函数触发连接请求,向服务器发送SYN J包,对应的应用进程进入阻塞状态。
  2. 服务器调用listen()函数后开始监听连接请求,并调用accept()函数对连接请求做出响应,对应的应用进程进入阻塞状态。服务器收到SYN J包后,accpet()函数做出响应,向客户发送SYN K, ACK J+1包。
  3. 客户收到服务器的响应后(即收到SYN K, ACK J+1包后),connect()函数返回,并向服务器发送ACK K+1包进行确认。服务器收到ACK K+1包后,accept()函数返回。

在这里插入图片描述

  • accept()函数和listen()函数的先后关系:
  1. accept()函数是从listen()函数维护的队列中中获取已完成三次握手的连接,没有就会阻塞。
  2. 如果没有调用accept()函数,最后连接队列会爆满,导致新的连接请求对应的SYN分节会被忽略,或者服务器直接返回RST。
  3. 如果SYN分节被忽略,则客户会超时重传连接请求,遇上连接队列不满时,connect()函数执行成功返回0。
  4. 如果服务器直接返回RST,则connect()函数执行失败返回-1。
  5. connet()函数与accepet()函数的调用,二者没有必然的先后关系。只是如果没有调用accept()函数,会影响connect()函数的结果。
⑤ socket中TCP的四次挥手
  • socket中TCP的四次挥手:
  1. 主机1上的应用进程调用close()函数将socket标记为关闭状态,并向主机2发送 FIN M释放报文。这时,主机1进入FIN-WAIT1状态。
  2. 主机2收到FIN M释放报文后,返回ACK M+1报文进行确认,自己进入CLOSE_WAIT状态。主机1收到确认后,进入FIN-WAIT2状态。这时主机1不能再向主机2发送数据
  3. 主机2上的应用进程调用close()函数将socket标记为关闭状态,并向主机1发送FIN N释放报文。这时,主机2进入LAST-ACK状态。
  4. 主机1收到FIN N释放报文后,返回ACK N+1进行确认,自己进入TIME-WAIT状态,等待至少2MSL后,进入CLOSED状态。主机2收到确认后,进入CLOSED状态。
    在这里插入图片描述
  • 注意:close()函数将socket标记为关闭状态时,会让对应socket描述符的引用计数减1,直到引用计数为0才会触发四次挥手
  • 关于shutdown()函数:
int shutdown(int sockfd, int howto) // 成功返回0,失败返回-1
  • shutdown()函数的howto参数,可以指定关闭某个方向的数据传送。
  1. SHUT_RD: 关闭连接的读这一半,socket接收缓冲区的现有数据都被丢弃,应用进程不能再对socket调用任何的读函数。如果一个socket关闭读连接后,收到其他socket传来的数据都会确认,然后悄然丢弃。
  2. SHUT_WR: 关闭连接的写这一半,socket发送缓冲区中的数据将被发送掉,应用进程不能再对socket调用任何写函数。
  3. SHUT_RDWR: socket连接的读和写都被关闭,等价于先后调用两次shutdown()函数,并指定howto参数为SHUT_RDSHUT_WR
  • shutdown与close的区别:
  1. 关闭socket的时机不同: close()函数让socket的引用计数减1,直到为0才能触发TCP的四次挥手;shutdown()函数不管引用计数为多少,都能触发TCP的四次挥手。
  2. 全关闭与半关闭: close终止读和写两个方向的数据传送,shutdown一次可以只关闭一个方向的数据传送。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值