前言
平常每周都会有一些心得感悟,这些在大家工作中可能会有许多共性。我觉得花一些时间整理一下,跟粉丝读者们分享一下日常学习工作的想法和所得,这是一个很好的互动和文章分享的痛点。
这是第十五篇。
Linux IO
Linux IO机制中,操作系统讲IO数据缓存在文件系统的页缓存中,数据先拷贝到内核空间内,然后再从内核空间拷贝到用户空间使用。比如常用的网络socket的实现,数据从网卡接收到之后,保存到内核空间对应的缓存中,然后通过中断机制通知用户态程序,用户态程序使用read操作把数据拷贝到用户空间,这个就是所谓的缓存IO。 实现分为两步,第一显示等待数据准备,第二讲数据从内核拷贝到进程中。
Linux系统有五种网络模式方案:
- 阻塞IO
- 非阻塞IO
- IO多路复用
- 信号驱动IO
- 异步IO
阻塞IO
默认情况下,socket都是阻塞的。调用recvfrom的时候应用程序阻塞住,等待内核准备好数据之后,拷贝内核数据到用户缓冲区之后才继续执行。 阻塞IO的特点就是用户程序会block。
非阻塞IO
通过设置socket使其变为non-blocking的。当进程调用read的时候,如何内核中没有数据,用户程序并不会阻塞住,而是直接返回一个错误。通过判断错误,用户程序知道没有获取到数据,然后可以继续下一次read。一旦内核中有数据,在下一个调用read的时候就会返回数据了。非阻塞IO的特点就是用户程序不会block。
IO多路复用
IO多路复用就是平时遇到的select、poll、epoll等接口。一个进程同时可以监听多个socket,当任意一个socket有数据时,函数就会返回,然后就可以读取socket的数据然后处理。基本原理就是这些函数会不断的轮询所有负责的socket,然后返回数据给用户程序。
这个流程分为两步,首先要等待某个socket有数据,然后获取到具体的socket描述符,然后再通过read函数读取数据。这里需要两个系统调用,select和read。优势就是在同一个流程里面,实现了对多个socket的监听。一般用户程序是被select阻塞住,会设置socket为non-blocking。
异步IO
用户程序发起异步读的操作之后,就继续其他的处理了。等内核有数据到达后,会将数据拷贝到用户缓冲,然后通过信号的方式通知用户程序,用户程序在信号回调中处理数据。
信号驱动IO
这个用户程序就是完全被动的接收数据。当内核中有数据之后,调用对应的回调函数读取数据处理。
select、poll和epoll
select
1 | int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
select 函数可以监听三个类型的文件描述符, readfs, writefs, exceptfds。在调用select时程序阻塞住,一直到有描述符有事件产生或者select超时,select函数返回后遍历fds,查找就绪的描述符。select的一个确定就是监听的最大描述符数目有限定为1024, 当然也是可以设置的。
poll
1 | int poll (struct pollfd *fds, unsigned int nfds, int timeout); |
poll使用pollfd的结构来描述等待的信息。pollfd并没有最大数量限制,和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
1 | int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大 |
int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
1 | struct epoll_event { |
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
今天花时间分别实现了以上三种方式的socket监听,可以参考代码:
https://gitee.com/fishmwei/blog_code/blob/master/socket/iomode.c
更多
这周比较忙,在不断的输入新的概念和知识,也有一些旧的知识在眼前浮现。Linux的IO模型在编码的时候都会遇到,今天重新复习一下,并一一实现,进一步巩固一下知识点。
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。