「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不会阻塞进程,而是 立即返回一个错误码(通常是 EAGAINEWOULDBLOCK)。

例如,终端设置为 非阻塞状态下,使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main(){
int new_stdin;
char buf[256];
ssize_t nbytes;

// 以非阻塞只读方式重新打开终端文件
// /dev/tty 是一个特殊文件,总是指向当前进程的控制终端
// 添加 O_NONBLOCK 参数以非阻塞打开
new_stdin = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if(new_stdin == -1){
perror("open /dev/tty failed");
exit(EXIT_FAILURE);
}

// 将新打开的非阻塞终端文件描述符复制到标准输入位置(0)
// dup2() 关闭旧的 fd 0,然后将 new_stdin 复制到 fd 0 上
if(dup2(new_stdin, STDIN_FILENO) == -1){
perror("dup2 failed");
close(new_stdin);
exit(EXIT_FAILURE);
}

// 此时不再需要 new_stdin,可以直接使用 STDIN_FILENO
close(new_stdin);

printf("Standard input has been replaced and is now NON-BLOCKING.\n");

// 重复 5 次尝试读取
int i;
for(i = 0; i < 5; i ++ ){
nbytes = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if(nbytes == -1){
// 读取失败,判断是不是因为非阻塞导致的“暂无数据”
if(errno == EAGAIN || errno == EWOULDBLOCK){
printf("Try again\n");
sleep(3);
}else{
perror("read failed");
exit(EXIT_FAILURE);
}
}else{
// 读取成功,在重复尝试期间有数据输入
buf[nbytes] = '\0';
break;
}
}

// 判断是否超时
if(i == 5){
printf("Time out\n");
}else{
printf("Read %zd bytes: %s\n", nbytes, buf);
}

return 0;
}

在上面的代码中,在将终端设置为非阻塞状态后,重复 5 次尝试读取,每次尝试过后通过 sleep(3) 休息 3 秒。如果当前尝试没有数据,会输出 Try again 然后进行下一次尝试。若中途有数据输入,那么会输出输入的数据;否则直到循环结束,最后超时,输出 Time out

运行结果如下:


3 使用 fcntl 实现终端非阻塞

3.1 fcntl 函数

fcntl(file control)是一个非常重要的系统调用,用于对 已打开的文件描述符 进行各种 控制操作,包括 获取或设置文件描述符的状态标志阻塞与非阻塞属性是这些状态标志之一

函数原型

1
2
3
4
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
  • fd:要操作的文件描述符。
  • cmd:控制命令,指定要执行的操作类型。
  • ...:可变参数,根据 cmd 的不同而不同。

设置非阻塞的命令

  • F_GETFL(Get flags)

    获取文件描述符的当前状态标志(如 O_RDONLY, O_WRONLY, O_NONBLOCK 等)。

    1
    2
    3
    4
    5
    int 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main(){
char buf[256];
ssize_t nbytes;

// 获取当前标准输入的标志
int flags = fcntl(STDIN_FILENO, F_GETFL);
if(flags == -1){
perror("fcntl(F_GETFL) failed");
exit(EXIT_FAILURE);
}

// 在当前标志的基础上添加 O_NONBLOCK 标志
if(fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1){
perror("fcntl(F_SETFL) failed");
exit(EXIT_FAILURE);
}

printf("Standard input is now in NON-BLOCKING mode.\n");

// 重复 5 次尝试读取
int i;
for(i = 0; i < 5; i ++ ){
nbytes = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if(nbytes == -1){
// 读取失败,判断是不是因为非阻塞导致的“暂无数据”
if(errno == EAGAIN || errno == EWOULDBLOCK){
printf("Try again\n");
sleep(3);
}else{
// 其他错误
perror("read failed");
exit(EXIT_FAILURE);
}
}else{
// 读取成功,在重复尝试期间有数据输入
buf[nbytes] = '\0';
break;
}
}

// 判断是否超时
if(i == 5){
printf("Time out\n");
}else{
printf("Read %zd bytes: %s\n", nbytes, buf);
}

return 0;
}

运行结果如下:


「Linux 系统编程」阻塞与非阻塞、使用 fcntl 设置非阻塞
https://marisamagic.github.io/2025/08/22/20250822_2/
作者
MarisaMagic
发布于
2025年8月22日
许可协议