【Linux Socket C++】为什么IO复用需要用到非阻塞IO?EAGAIN的简单介绍与应用

8 篇文章 1 订阅

目录

为什么IO复用需要非阻塞的IO

EAGAIN的介绍

EAGAIN的应用


为什么IO复用需要非阻塞的IO

我们可以先看一下官方的回答:

在Linux命令行输入:man 2 select

找到[BUGS],如下:

官方给予的回答是这样的:

Under  Linux, select() may report a socket file descriptor as "ready for reading", while never? theless a subsequent read blocks.  This could for example happen when data has arrived but upon examination  has  wrong checksum and is discarded.  There may be other circumstances in which a file descriptor is spuriously reported as ready.  Thus it may be safer  to  use  O_NONBLOCK  on sockets that should not block.

也就是说:当某个socket接收缓冲区有新数据分节到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误,然后丢弃这个分节,这时候调用read则无数据可读,如果socket没有设置nonblocking,此read将阻塞当前线程

简单来说就是:当数据到达socket缓冲区的时候,select会报告这个socket可读,但是随后因为一些原因,比如校验和错误,内核丢弃了这个数据,这个时候,如果采用了阻塞的IO,唤醒的程序去读取一个已经被丢弃的数据,肯定读不到,所以就会一直卡在那里,也就是阻塞,例如accept和recv函数就会阻塞。

所以这也就是IO复用需要非阻塞IO的原因之一。

原因二:达到缓冲区的数据有可能被别人抢走,比如多个进程accept同一个socket时引发的惊群现象,只有一个客户端连接到来,但是所有的监听程序都会被唤醒,最终只能有一个进程可以accept到这个请求,如果采用阻塞的IO,其他进程的accept就会产生阻塞。

原因三:ET(Edge-triggered)边缘模式下,必须要使用到非阻塞的IO,因为程序中需要循环读和写,直到EAGAIN的出现,如果使用阻塞的IO就容易被阻塞住。

EAGAIN的介绍

这里提一下有关EAGAIN的知识

EAGAIN是一个错误代码,在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。

man accept找到EAGAIN的解释如下:

原文是这样说的:

The  socket  is  marked  nonblocking  and  no  connections  are  present to be accepted.
POSIX.1-2001 allows either error to be returned for this  case,  and  does  not  require these  constants to have the same value, so a portable application should check for both
possibilities. 

翻译过来的意思大概就是:如果socket的状态为非阻塞,但是accept函数没有找到可用的连接,就会返回EAGAIN错误。

我们时常会用一个while(true)死循环去接收缓冲区中客户端socket的连接,如果这个时候我们设置socket状态为非阻塞,那么accept如果在某个时间段没有接收到客户端的连接,因为是非阻塞的IO,accept函数会立即返回,并将errno设置为EAGAIN.

EAGAIN的应用

EAGAIN应用示例一:accept

我们时常会在死循环中设置if(errno==EAGAIN) break;

因为我们死循环只是为了接收缓冲区中的连接,一旦accept在缓冲区找不到可用的连接了,那么accept会将errno设置为EAGAIN,这个时候我们只需要判断errno==EAGAIN就说明了,accept已经将缓冲区中的连接读取完,所以可以直接break了。

EAGAIN应用示例二:recv、send

前提:非阻塞的IO、EPOLLET边缘触发模式

recv:在EPOLLIN|EPOLLET监视可读、边缘触发模式下,recv函数会时刻关注缓冲区中是否有数据可读,如果缓冲区中有数据未处理,EPOLLET模式下的epoll只会汇报一次socket有可读事件,当有新的数据加入缓冲区时,就会再次汇报可读。

根据上面的说明,假设现在缓冲区中有1000字节待recv读取,ET模式下会触发一次可读事件,为了避免我们我们读取不完缓冲区的数据(也就是说假设我们的recv目前一次只能读取200字节,所以一次肯定读取不玩),这个时候我们又会使用到死循环while(true),让recv一直读取缓冲区中的内容。我们知道缓冲区如果没有新的内容,是不会再触发可读事件的,所以当recv读取完缓冲区的内容之后,因为是非阻塞的IO模式,recv不会等待立即返回,并且会将errno设置为EAGAIN,此时的EAGAIN表示没有数据可以读取。

因此,我们可以像accept函数那样,在死循环里判断errno,也就是if(errno==EAGAIN) break;

send:在EPOLLOUT|EPOLLET监视可写、边缘触发模式下,send函数会时刻关注发送缓冲区是否已满,如果发送缓冲区未满,EPOLLET模式下就会触发一次可写事件,只有当缓冲区从满变为"有空"的时候,才会再次触发一次可写事件(假设缓冲区大小为1000,缓冲区中有300字节内容,客户端读取比服务端发送要快,这个时候缓冲区内容减少,但不会触发可写事件)。

根据上面的说明,假设缓冲区1000字节就满了,现如今缓冲区中有300字节的内容,那么ET模式会触发一次可写事件,在这个可写时间的处理代码中,你可以向客户端发送你想要发送的内容。当写入缓冲区满了的时候,因为是非阻塞IO,send会立即返回,并将errno设置为EAGAIN,此时的EAGAIN表示缓冲区内容已满,无法继续写入,所以我们也没必要让send阻塞在那里。

因此,我们可以像accept函数那样,当然循环的出口还是我们说的if(errno==EAGAIN) break;

EAGAIN这个错误代码在非阻塞IO的程序中会经常出现,上面只是一些经常使用到EAGAIN的场景;有时我们可以不把它看作是一个错误,而看作是一个工具,一个来寻找循环出口的工具。

以上便是文章的全部内容,如果讲解有误,还请指正,谢谢大家观看!

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要让字符设备驱动支持阻塞与非阻塞IO模型,需要在驱动程序中使用select/poll机制,同时使用文件操作的O_NONBLOCK标志。下面以read操作为例,简要说明如下: 1. 在设备驱动程序中,使用`file_operations`结构体中的`read`函数实现读操作,同时在该函数中添加对阻塞和非阻塞IO模型的支持。 2. 对于阻塞IO模型,可以直接在`read`函数中调用`wait_event_interruptible`函数使进程进入睡眠状态,等待数据就绪后再唤醒进程。当然在等待数据就绪的过程中,如果进程接收到了信号,则需要立即返回`-ERESTARTSYS`。 3. 对于非阻塞IO模型,需要在`read`函数中使用`O_NONBLOCK`标志进行判断。如果该标志被设置,则可以直接调用`poll_wait`函数等待数据就绪,如果数据没有准备好,则直接返回`-EAGAIN`。 4. 在`poll`函数中,需要添加对于设备文件的监控,以便在数据就绪时通知进程。这可以通过在驱动程序中添加`poll`函数来实现。在该函数中,需要使用`poll_wait`函数将当前进程添加到等待队列中,并在数据就绪时唤醒进程。 下面是一个简单的代码示例: ```c static DECLARE_WAIT_QUEUE_HEAD(read_wait_queue); static unsigned int mydevice_poll(struct file *file, poll_table *wait) { unsigned int mask = 0; struct mydevice_data *data = file->private_data; poll_wait(file, &read_wait_queue, wait); if (data->data_ready) { mask |= POLLIN | POLLRDNORM; } return mask; } static ssize_t mydevice_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { struct mydevice_data *data = file->private_data; ssize_t ret = 0; if (file->f_flags & O_NONBLOCK) { /* 非阻塞IO模型 */ if (!data->data_ready) { return -EAGAIN; } } else { /* 阻塞IO模型 */ wait_event_interruptible(read_wait_queue, data->data_ready); if (signal_pending(current)) { return -ERESTARTSYS; } } /* 读取数据 */ if (copy_to_user(buf, data->buffer, data->size)) { ret = -EFAULT; } else { ret = data->size; data->data_ready = 0; } return ret; } ``` 在上面的代码示例中,`mydevice_read`函数是`read`操作对应的函数,`mydevice_poll`函数是`poll`操作对应的函数。其中,`wait_event_interruptible`函数用于阻塞进程,等待数据就绪;`poll_wait`函数用于将进程添加到等待队列中,等待数据就绪时唤醒进程。同时,根据文件操作的O_NONBLOCK标志,判断当前使用的是阻塞IO模型还是非阻塞IO模型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值