「Linux 系统编程」阻塞与非阻塞、使用 fcntl 设置非阻塞
1 阻塞与非阻塞
阻塞 与 非阻塞 两种模式决定了当一个进程进行 I/O 操作(如读 read
、写 write
)时,如果资源无法立即被获取,进程会进入 阻塞状态 ,还是 非阻塞 直接返回无数据信息。
产生阻塞的场景:读 设备文件(例如终端、管道);读 网络文件。(读常规文件无阻塞概念)
1.1 阻塞 I/O
当进程尝试进行一个不能立即完成的 I/O 操作时,进程会被置为 睡眠状态(休眠),直到等待的条件满足(如数据可读、缓冲区可写)或被信号中断。
例如,使用 read(STDIN_FILENO, buf, sizeof(buf))
从终端(标准输入)读取数据,如果一直不输入字符,程序会在 read
函数处 停下来,一直等待,直到在终端上实际输入了一些字符并按下回车键。
“停下来等待” 的过程就是 阻塞。进程放弃了 CPU,进入睡眠状态,不会浪费系统资源。
1.2 非阻塞 I/O
当文件描述符被设置为 非阻塞模式 后,如果 I/O 操作不能立即完成,系统调用(如 read
, write
)不会阻塞进程,而是 立即返回一个错误码(通常是 EAGAIN
或 EWOULDBLOCK
)。
例如,终端设置为 非阻塞状态下,使用 read(STDIN_FILENO, buf, sizeof(buf))
从终端(标准输入)读取数据,如果一直没有任何输入,read
调用会立刻返回 -1
,并 将 errno
设置为 EAGAIN
。
此时进程不会被挂起,进程可以继续去做其他事情(比如计算、尝试读取其他文件描述符等),过一会儿再来轮询这个终端是否有数据可读。
1.3 阻塞与非阻塞对比
特性 | 阻塞模式 (默认) | 非阻塞模式 |
---|---|---|
行为 | 等待,直到操作完成 | 立即返回,成功或失败 |
返回值 | 成功:读取的字节数;失败:-1 | 成功:读取的字节数;无数据:-1 (errno=EAGAIN) |
进程状态 | 睡眠(Sleep) | 运行(Running) |
CPU 占用 | 等待时不占用 CPU | 需要轮询,可能占用更多 CPU |
编程复杂度 | 低(逻辑简单直接) | 高(需要循环重试、处理错误) |
适用场景 | 普通顺序执行程序 | 高性能 I/O 多路复用(如 select , poll , epoll ) |
2 打开 /dev/tty 实现终端非阻塞
重新打开终端设备,并指定 O_NONBLOCK
标志,然后用这个新的非阻塞文件描述符替换掉原来的标准输入(0)。
Linux 系统为每个会话分配一个终端设备,可以通过 /dev/tty
这个特殊文件访问当前进程的控制终端。
以 非阻塞只读模式 重新打开这个终端设备,返回一个 新的文件描述符(比如 fd 3)。然后 dup2()
系统调用,将新打开的文件描述符复制到标准输入的文件描述符(0)上。从而 使得文件描述符 0(标准输入)处于非阻塞模式。
1 |
|
在上面的代码中,在将终端设置为非阻塞状态后,重复 5 次尝试读取,每次尝试过后通过 sleep(3)
休息 3 秒。如果当前尝试没有数据,会输出 Try again
然后进行下一次尝试。若中途有数据输入,那么会输出输入的数据;否则直到循环结束,最后超时,输出 Time out
。
运行结果如下:
3 使用 fcntl 实现终端非阻塞
3.1 fcntl 函数
fcntl
(file control)是一个非常重要的系统调用,用于对 已打开的文件描述符 进行各种 控制操作,包括 获取或设置文件描述符的状态标志。阻塞与非阻塞属性是这些状态标志之一。
函数原型:
1 |
|
fd
:要操作的文件描述符。cmd
:控制命令,指定要执行的操作类型。...
:可变参数,根据cmd
的不同而不同。
设置非阻塞的命令:
-
F_GETFL
(Get flags)获取文件描述符的当前状态标志(如 O_RDONLY, O_WRONLY, O_NONBLOCK 等)。
1
2
3
4
5int flags = fcntl(fd, F_GETFL);
if(flags == -1){
perror("fcntl get flags failed");
exit(EXIT_FAILURE);
} -
F_SETFL
(Set Flags)设置文件描述符的状态标志。需要第三个参数,即新的标志值。
注意:不能直接设置 flags。要先获取当前标志,然后在其基础上按位或 (|) 上需要添加的标志,再设置上去。(标志位中有一些是只读的,直接覆盖会丢失原有信息)
1
2
3
4
5
6// 在原有标志的基础上添加非阻塞标志 (O_NONBLOCK)
int new_flags = flags | O_NONBLOCK;
if (fcntl(fd, F_SETFL, new_flags) == -1) {
perror("fcntl set flags failed");
exit(EXIT_FAILURE);
}
3.2 fcntl 实现终端非阻塞
1 |
|
运行结果如下: