vlambda博客
学习文章列表

周谈(15)- Linux IO模型及实现

前言

平常每周都会有一些心得感悟,这些在大家工作中可能会有许多共性。我觉得花一些时间整理一下,跟粉丝读者们分享一下日常学习工作的想法和所得,这是一个很好的互动和文章分享的痛点。

这是第十五篇。

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

1int 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

1int poll (struct pollfd *fds, unsigned int nfds, int timeout);
2
3struct pollfd {
4    int fd; /* file descriptor */
5    short events; /* requested events to watch */
6    short revents; /* returned events witnessed */
7};

poll使用pollfd的结构来描述等待的信息。pollfd并没有最大数量限制,和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

1int epoll_create(int size)//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
2int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
3int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
;
  1. int epoll_create(int size);
    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
    当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

  2. 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结构如下:

 1struct epoll_event {
2  __uint32_t events;  /* Epoll events */
3  epoll_data_t data;  /* User data variable */
4};
5
6//events可以是以下几个宏的集合:
7EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
8EPOLLOUT:表示对应的文件描述符可以写;
9EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
10EPOLLERR:表示对应的文件描述符发生错误;
11EPOLLHUP:表示对应的文件描述符被挂断;
12EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
13EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  1. 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模型在编码的时候都会遇到,今天重新复习一下,并一一实现,进一步巩固一下知识点。花了一个晚上才把这个代码调试完成,时间很快,准备洗洗睡了。


行动,才不会被动!

个人博客:https://fishmwei.gitee.io/