Linux 阻塞非阻塞

发布于:2025-06-11 ⋅ 阅读:(25) ⋅ 点赞:(0)

linux open函数 在非阻塞模式下的使用

ChatGPT said:

在 Linux 中,open() 函数用于打开文件或设备。在**非阻塞模式(non-blocking mode)**下使用 open(),主要用于设备文件、FIFO、套接字等可能会导致阻塞的文件类型。常规的普通文件不会因为是否阻塞而行为不同。


一、使用方式

非阻塞模式的关键在于设置 O_NONBLOCK 标志。

示例代码:


c

CopyEdit

#include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <errno.h> int main() { int fd = open("/dev/ttyS0", O_RDONLY | O_NONBLOCK); // 示例:打开串口设备为非阻塞读 if (fd == -1) { perror("open"); return 1; } char buffer[100]; ssize_t n = read(fd, buffer, sizeof(buffer)); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("No data available now (non-blocking mode)\n"); } else { perror("read"); } } else if (n == 0) { printf("EOF reached\n"); } else { printf("Read %zd bytes\n", n); } close(fd); return 0; }


二、重点说明

  • O_NONBLOCKfcntl.h 中定义的宏,用来设置文件描述符为非阻塞模式。

  • 对于普通文件(如磁盘上的文本文件),O_NONBLOCK 没有实际效果,read() 总是会返回所有可读数据。

  • 对于如下类型文件特别有用:

    • 管道(FIFO)

    • 套接字(socket)

    • 设备文件(如 /dev/tty*, /dev/null

  • 如果你想在打开之后再设置非阻塞模式,可以使用 fcntl()


c

CopyEdit

int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK);


三、典型用途场景

  • 事件驱动编程(如 epoll/select/poll)
    配合 O_NONBLOCK 和 I/O 多路复用机制使用,避免阻塞,提高系统响应能力。

  • 设备驱动/串口通信/异步I/O
    读取设备数据时不阻塞主程序流程。

  • 服务端程序/并发服务器
    接收多个客户端连接,避免因单个阻塞操作卡死整个进程。


如果你有具体场景(比如管道、socket、串口等),我可以给出更详细的使用建议。

2/2

You said:

管道

ChatGPT said:

管道(pipe)在 Linux 下是一种常见的进程间通信(IPC)方式。管道的读写操作如果没有数据准备好,默认是阻塞的,也就是说:

  • 读端调用 read(),没有数据时会阻塞等待写入。

  • 写端调用 write(),管道缓冲区满时会阻塞等待空间。


管道的非阻塞模式使用

1. 打开或创建管道文件时加 O_NONBLOCK

  • 命名管道(FIFO),用 mkfifo() 创建后,可以用 open() 以非阻塞模式打开。

  • 匿名管道 是通过 pipe() 创建的文件描述符对,不能直接用 open(),但是可以用 fcntl() 设置非阻塞标志。


2. 示例代码:命名管道非阻塞读写

假设先用命令创建管道:


bash

CopyEdit

mkfifo myfifo

然后在程序里:


c

CopyEdit

#include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <errno.h> int main() { // 以非阻塞方式打开命名管道读端 int fd = open("myfifo", O_RDONLY | O_NONBLOCK); if (fd == -1) { perror("open"); return 1; } char buf[128]; ssize_t n = read(fd, buf, sizeof(buf)); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("No data available now (non-blocking read)\n"); } else { perror("read"); } } else if (n == 0) { printf("Writer closed pipe (EOF)\n"); } else { printf("Read %zd bytes: %.*s\n", n, (int)n, buf); } close(fd); return 0; }


3. 匿名管道非阻塞设置

假设用 pipe() 创建匿名管道:


c

CopyEdit

int pipefd[2]; pipe(pipefd); // pipefd[0] 是读端,pipefd[1] 是写端

设置非阻塞:


c

CopyEdit

#include <fcntl.h> int flags = fcntl(pipefd[0], F_GETFL, 0); fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK); flags = fcntl(pipefd[1], F_GETFL, 0); fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK);

这样读写端都变成非阻塞模式,读端没有数据时 read() 返回 -1 并设置 errno=EAGAIN,写端写满时 write() 也会返回 -1 并设置 errno=EAGAIN


4. 非阻塞管道常见行为

操作 数据状态 阻塞/非阻塞行为
读端读数据 管道无数据 阻塞模式:阻塞;非阻塞:返回 -1,errno=EAGAIN
读端读数据 有数据 返回数据量
写端写数据 管道缓冲区满 阻塞模式:阻塞;非阻塞:返回 -1,errno=EAGAIN
写端写数据 有空间 写入数据量


如果你想,我还可以帮你写一个完整的读写管道非阻塞demo。你需要吗?

You said:

需要

ChatGPT said:

明白!下面是一个简单的 匿名管道 的非阻塞读写演示程序。演示父进程写管道,子进程非阻塞读管道的情况。


c

CopyEdit

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> int set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; } int main() { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); return 1; } pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程 - 读端 close(pipefd[1]); // 关闭写端 // 设置读端为非阻塞 if (set_nonblocking(pipefd[0]) == -1) { exit(1); } char buf[100]; while (1) { ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("子进程:管道暂无数据,继续等待...\n"); sleep(1); continue; } else { perror("read"); break; } } else if (n == 0) { printf("子进程:管道写端关闭,结束读取\n"); break; } else { buf[n] = '\0'; printf("子进程:读取到数据: %s\n", buf); } } close(pipefd[0]); exit(0); } else { // 父进程 - 写端 close(pipefd[0]); // 关闭读端 const char *msg = "Hello from parent!\n"; for (int i = 0; i < 5; i++) { ssize_t n = write(pipefd[1], msg, strlen(msg)); if (n == -1) { perror("write"); break; } printf("父进程:写入数据: %s", msg); sleep(2); } close(pipefd[1]); // 关闭写端,通知子进程EOF wait(NULL); // 等待子进程结束 } return 0; }


说明:

  • 父进程写端写数据,每隔 2 秒写一次;

  • 子进程读端设置为非阻塞模式,read() 无数据时不会阻塞,而是返回 -1 并设置 errno = EAGAIN,这时子进程打印等待信息并继续轮询;

  • 父进程关闭写端后,子进程读端读到 0 字节,表示管道关闭,退出循环。


你可以把这个代码保存为 pipe_nonblock.c,用 gcc pipe_nonblock.c -o pipe_nonblock 编译,然后运行看效果。

需要我帮你讲解代码每个部分的细节吗?

You said:

需要

ChatGPT said:

好的,我来帮你详细讲解这个非阻塞管道读写程序的每个关键部分:


1. set_nonblocking 函数


c

CopyEdit

int set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; }

  • 作用:给指定的文件描述符 fd 添加非阻塞标志 O_NONBLOCK

  • fcntl(fd, F_GETFL, 0):获取当前文件描述符状态标志。

  • fcntl(fd, F_SETFL, flags | O_NONBLOCK):在原有标志基础上加上非阻塞标志。


2. 创建匿名管道


c

CopyEdit

int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); return 1; }

  • pipefd[0] 是读端,pipefd[1] 是写端。

  • 管道是一对文件描述符,进程间通过它们传输数据。


3. 创建子进程


c

CopyEdit

pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程 ... } else { // 父进程 ... }

  • fork() 复制当前进程,返回值用来区分父子进程。


4. 子进程处理(读端)


c

CopyEdit

close(pipefd[1]); // 关闭写端 set_nonblocking(pipefd[0]); // 设置读端非阻塞 char buf[100]; while (1) { ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("子进程:管道暂无数据,继续等待...\n"); sleep(1); // 等待一秒再尝试读,避免CPU空转 continue; } else { perror("read"); break; } } else if (n == 0) { printf("子进程:管道写端关闭,结束读取\n"); break; } else { buf[n] = '\0'; printf("子进程:读取到数据: %s\n", buf); } } close(pipefd[0]);

  • 关闭写端,因为子进程只读。

  • 通过 set_nonblocking 把读端设成非阻塞。

  • 调用 read() 读数据:

    • 如果返回 -1 并且 errnoEAGAIN/EWOULDBLOCK,说明当前没有数据,不阻塞,打印提示并继续轮询。

    • 返回 0 表示写端关闭,读端读到 EOF,退出循环。

    • 返回 >0,说明读到数据,打印出来。

  • 轮询读取的方式适合没有事件通知机制的简单示例,实际开发中可以结合 selectpollepoll


5. 父进程处理(写端)


c

CopyEdit

close(pipefd[0]); // 关闭读端 const char *msg = "Hello from parent!\n"; for (int i = 0; i < 5; i++) { ssize_t n = write(pipefd[1], msg, strlen(msg)); if (n == -1) { perror("write"); break; } printf("父进程:写入数据: %s", msg); sleep(2); } close(pipefd[1]); // 关闭写端,通知子进程EOF wait(NULL); // 等待子进程结束

  • 关闭读端,父进程只写管道。

  • 每隔 2 秒写一次消息,一共写 5 次。

  • 关闭写端后,子进程读端会感知到 EOF。

  • wait(NULL) 等待子进程结束,避免僵尸进程。


6. 程序运行效果总结

  • 子进程因为非阻塞读,没有数据时不会阻塞,而是打印“暂无数据”继续轮询。

  • 父进程定时写入数据,子进程能立即读到并打印。

  • 父进程结束写入并关闭写端后,子进程检测到 EOF 并退出。


如果你想,我也可以帮你写一个基于 selectpoll 的非阻塞管道事件监听版本,避免轮询浪费CPU资源。你感兴趣吗?

You said:

可以

ChatGPT said:

好的!下面给你一个基于 select() 的非阻塞匿名管道读写示例,避免子进程不停轮询,只有管道有数据时才读:


c

CopyEdit

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> #include <sys/select.h> #include <sys/wait.h> int set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; } int main() { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); return 1; } pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程,读端 close(pipefd[1]); if (set_nonblocking(pipefd[0]) == -1) { exit(1); } char buf[100]; fd_set readfds; while (1) { FD_ZERO(&readfds); FD_SET(pipefd[0], &readfds); int ret = select(pipefd[0] + 1, &readfds, NULL, NULL, NULL); if (ret == -1) { perror("select"); break; } if (FD_ISSET(pipefd[0], &readfds)) { ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 理论上select返回就绪,这里不会出现这个情况 continue; } else { perror("read"); break; } } else if (n == 0) { printf("子进程:管道写端关闭,退出\n"); break; } else { buf[n] = '\0'; printf("子进程读取: %s", buf); } } } close(pipefd[0]); exit(0); } else { // 父进程,写端 close(pipefd[0]); const char *msg = "Hello from parent via select!\n"; for (int i = 0; i < 5; i++) { ssize_t n = write(pipefd[1], msg, strlen(msg)); if (n == -1) { perror("write"); break; } printf("父进程写入: %s", msg); sleep(2); } close(pipefd[1]); wait(NULL); } return 0; }


说明

  • select() 会阻塞等待管道变为可读,避免了轮询浪费 CPU。

  • 一旦管道有数据,select 返回,子进程读数据。

  • 写端关闭后,读端 read() 返回 0,子进程退出。

  • 子进程依然设置了非阻塞模式,配合 select 使用,保证读操作不会意外阻塞。


想试试吗?或者需要改成 poll / epoll 版本也可以跟我说!

You said:

需要

ChatGPT said:

好的,这里给你一个基于 poll() 的非阻塞管道读写示例。pollselect 类似,但更灵活,适合监听大量文件描述符。


c

CopyEdit

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> #include <poll.h> #include <sys/wait.h> int set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; } int main() { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); return 1; } pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程,读端 close(pipefd[1]); if (set_nonblocking(pipefd[0]) == -1) { exit(1); } struct pollfd pfd; pfd.fd = pipefd[0]; pfd.events = POLLIN; char buf[100]; while (1) { int ret = poll(&pfd, 1, -1); // 阻塞等待 if (ret == -1) { perror("poll"); break; } if (pfd.revents & POLLIN) { ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { continue; } else { perror("read"); break; } } else if (n == 0) { printf("子进程:管道写端关闭,退出\n"); break; } else { buf[n] = '\0'; printf("子进程读取: %s", buf); } } } close(pipefd[0]); exit(0); } else { // 父进程,写端 close(pipefd[0]); const char *msg = "Hello from parent via poll!\n"; for (int i = 0; i < 5; i++) { ssize_t n = write(pipefd[1], msg, strlen(msg)); if (n == -1) { perror("write"); break; } printf("父进程写入: %s", msg); sleep(2); } close(pipefd[1]); wait(NULL); } return 0; }


说明

  • poll() 阻塞等待管道读端可读事件。

  • 有数据时读取,不阻塞。

  • 写端关闭,read() 返回 0,子进程退出。

  • 代码结构和 select() 版本类似,但 pollstruct pollfd 传递监控信息,更适合多文件描述符场景。


需要我帮你写一个 epoll 版本吗?或者帮你解释这个 poll 示例?

You said:

需要

ChatGPT said:

好嘞!下面是基于 epoll 的非阻塞匿名管道读写示例。

epoll 是 Linux 下更高效的多路复用机制,适合监听大量文件描述符,性能优于 selectpoll


c

CopyEdit

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> #include <sys/epoll.h> #include <sys/wait.h> int set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; } int main() { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); return 1; } pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程,读端 close(pipefd[1]); if (set_nonblocking(pipefd[0]) == -1) { exit(1); } int epfd = epoll_create1(0); if (epfd == -1) { perror("epoll_create1"); exit(1); } struct epoll_event event; event.events = EPOLLIN; event.data.fd = pipefd[0]; if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &event) == -1) { perror("epoll_ctl"); exit(1); } struct epoll_event events[10]; char buf[100]; while (1) { int nready = epoll_wait(epfd, events, 10, -1); if (nready == -1) { perror("epoll_wait"); break; } for (int i = 0; i < nready; i++) { if (events[i].data.fd == pipefd[0]) { if (events[i].events & EPOLLIN) { ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { continue; } else { perror("read"); goto cleanup; } } else if (n == 0) { printf("子进程:管道写端关闭,退出\n"); goto cleanup; } else { buf[n] = '\0'; printf("子进程读取: %s", buf); } } } } } cleanup: close(pipefd[0]); close(epfd); exit(0); } else { // 父进程,写端 close(pipefd[0]); const char *msg = "Hello from parent via epoll!\n"; for (int i = 0; i < 5; i++) { ssize_t n = write(pipefd[1], msg, strlen(msg)); if (n == -1) { perror("write"); break; } printf("父进程写入: %s", msg); sleep(2); } close(pipefd[1]); wait(NULL); } return 0; }


重点说明:

  • epoll_create1() 创建 epoll 实例。

  • epoll_ctl() 将管道读端添加到 epoll 监听列表,监听 EPOLLIN(可读事件)。

  • epoll_wait() 阻塞等待事件发生,只有有数据可读时才返回。

  • 读取数据,EOF(read 返回 0)时退出。

  • 子进程的管道读端设置为非阻塞。

  • 父进程写端按周期写入数据,写完关闭。


你可以试试,觉得如何?
还想了解 epoll 细节,或者要帮你写基于 socket 的 epoll 示例也行。

You said:

接收和发送端可以同时用open打开并且设置为非阻塞模式吗?

ChatGPT said:

可以的!

在 Linux 下,管道的读端和写端确实可以分别用 open() 打开(或者创建后继承),并且都设置成非阻塞模式,这样读写操作就不会阻塞进程。


具体说明:

  • 管道本质上是文件描述符,和普通文件一样都能用 fcntl() 设置非阻塞标志 O_NONBLOCK

  • 读端设置非阻塞时,read() 在无数据时不会阻塞,直接返回 -1,errno = EAGAINEWOULDBLOCK

  • 写端设置非阻塞时,write() 在管道缓冲区满时不会阻塞,直接返回 -1,errno = EAGAIN

  • 这对需要异步读写的程序很有用,比如事件驱动模型(使用 select / poll / epoll)。

  • 如果是通过文件(FIFO 命名管道)而非匿名管道,也可以用 open() 打开读写端,非阻塞方式同理。


举例:


c

CopyEdit

int fd_read = open("fifo_pipe", O_RDONLY | O_NONBLOCK); int fd_write = open("fifo_pipe", O_WRONLY | O_NONBLOCK);

或者匿名管道后用 fcntl 设置:


c

CopyEdit

fcntl(fd_read, F_SETFL, O_NONBLOCK); fcntl(fd_write, F_SETFL, O_NONBLOCK);


注意点:

  • 非阻塞写入时,如果管道缓冲区满,写操作会失败并返回 EAGAIN,这时一般要等到缓冲区有空再写,或者用 select/poll/epoll 监听写事件。

  • 非阻塞读取时,没有数据会立即返回,不阻塞。

  • 不能保证读写同时一定成功,应用层要处理这些边界情况。


总结:
接收端和发送端都可以同时以非阻塞模式打开管道并操作,这是完全支持且常用的。

如果你想,我可以帮你写一个基于命名管道(FIFO),读写端都非阻塞的示例程序。需要吗?

You said:

需要

ChatGPT said:

好的!下面给你一个基于**命名管道(FIFO)**的示例,接收端和发送端都用open()打开,并且都设置为非阻塞模式。


1. 创建命名管道(只需执行一次,或者放到程序外面)


bash

CopyEdit

mkfifo /tmp/myfifo


2. 发送端程序(写端,非阻塞)


c

CopyEdit

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { int fd = open("/tmp/myfifo", O_WRONLY | O_NONBLOCK); if (fd == -1) { perror("open fifo for write"); return 1; } const char *msg = "Hello from writer via FIFO\n"; for (int i = 0; i < 5; i++) { ssize_t n = write(fd, msg, strlen(msg)); if (n == -1) { if (errno == EAGAIN) { printf("写入缓冲区满,稍后重试...\n"); usleep(500000); // 0.5秒后重试 i--; // 重试这次写入 continue; } else { perror("write"); break; } } printf("写入数据: %s", msg); sleep(1); } close(fd); return 0; }


3. 接收端程序(读端,非阻塞)


c

CopyEdit

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK); if (fd == -1) { perror("open fifo for read"); return 1; } char buf[128]; while (1) { ssize_t n = read(fd, buf, sizeof(buf)-1); if (n == -1) { if (errno == EAGAIN) { printf("暂无数据,继续等待...\n"); usleep(500000); // 0.5秒 continue; } else { perror("read"); break; } } else if (n == 0) { printf("写端关闭,结束读取\n"); break; } else { buf[n] = '\0'; printf("读取数据: %s", buf); } } close(fd); return 0; }


使用说明:

  • 先确保执行过 mkfifo /tmp/myfifo,创建命名管道文件。

  • 分别在两个终端编译运行发送端和接收端程序。

  • 都用了 O_NONBLOCK,写端写缓冲区满会返回 EAGAIN,写端程序做了简单重试处理。

  • 读端没数据时也不会阻塞,会循环等待。


如果你需要,我也可以帮你写一个用 selectpoll 监听命名管道可读可写事件的版本。你需要吗?

You said:

先确保执行过 mkfifo /tmp/myfifo,创建命名管道文件 是什么意思

ChatGPT said:

我说的“先确保执行过 mkfifo /tmp/myfifo,创建命名管道文件”意思是:

命名管道(FIFO)是一种特殊的文件,它像普通文件一样有路径(比如 /tmp/myfifo),但它是内核提供的管道通信机制。要使用命名管道,必须先创建这个特殊的文件。


怎么创建?

你需要在终端(命令行)执行:


bash

CopyEdit

mkfifo /tmp/myfifo

这条命令会在 /tmp 目录下创建一个名叫 myfifo 的命名管道文件。


为什么要先创建?

  • 程序里用 open("/tmp/myfifo", ...) 打开这个文件。

  • 如果文件不存在,open() 会失败,程序无法正常运行。

  • 所以先用 mkfifo 命令创建它。


你可以这样操作:

  1. 打开一个终端,执行:

     

    bash

    CopyEdit

    mkfifo /tmp/myfifo

  2. 然后编译运行我之前给你的两个程序(读和写程序),它们会通过这个管道文件通信。


总结就是:

命名管道文件是内核的一种通信端点,要先用 mkfifo 创建,程序才能打开和使用它。


如果你想,我也可以帮你写个程序代码版本自动创建命名管道(用 mkfifo() 系统调用),你要吗?