Linux 阻塞非阻塞
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_NONBLOCK
是fcntl.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 并且
errno
是EAGAIN
/EWOULDBLOCK
,说明当前没有数据,不阻塞,打印提示并继续轮询。 -
返回 0 表示写端关闭,读端读到 EOF,退出循环。
-
返回 >0,说明读到数据,打印出来。
-
-
轮询读取的方式适合没有事件通知机制的简单示例,实际开发中可以结合
select
、poll
或epoll
。
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 并退出。
如果你想,我也可以帮你写一个基于 select
或 poll
的非阻塞管道事件监听版本,避免轮询浪费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() 的非阻塞管道读写示例。poll
和 select
类似,但更灵活,适合监听大量文件描述符。
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()
版本类似,但poll
用struct pollfd
传递监控信息,更适合多文件描述符场景。
需要我帮你写一个 epoll
版本吗?或者帮你解释这个 poll
示例?
You said:
需要
ChatGPT said:
好嘞!下面是基于 epoll 的非阻塞匿名管道读写示例。
epoll
是 Linux 下更高效的多路复用机制,适合监听大量文件描述符,性能优于 select
和 poll
。
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 =EAGAIN
或EWOULDBLOCK
。 -
写端设置非阻塞时,
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
,写端程序做了简单重试处理。 -
读端没数据时也不会阻塞,会循环等待。
如果你需要,我也可以帮你写一个用 select
或 poll
监听命名管道可读可写事件的版本。你需要吗?
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
命令创建它。
你可以这样操作:
-
打开一个终端,执行:
bash
CopyEdit
mkfifo /tmp/myfifo
-
然后编译运行我之前给你的两个程序(读和写程序),它们会通过这个管道文件通信。
总结就是:
命名管道文件是内核的一种通信端点,要先用
mkfifo
创建,程序才能打开和使用它。
如果你想,我也可以帮你写个程序代码版本自动创建命名管道(用 mkfifo()
系统调用),你要吗?