Linux FIFO(命名管道)与I/O多路复用编程笔记
一、基础FIFO通信
1. 基本概念
-
FIFO(命名管道):存在于文件系统中的特殊文件,用于进程间通信
-
特点:
-
半双工通信(单向)
-
需要读写双方都打开才能正常通信
-
阻塞式I/O(默认)
-
数据按先进先出顺序传输
-
2. 基础实现
写入端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[] = "hello,friend...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
读取端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_RDONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[1024] = {0};
read(fd, buf, sizeof(buf));
printf("fifo_w:%s\n", buf);
fgets(buf,sizeof(buf),stdin);
printf("term:%s",buf);
fflush(stdout);
}
close(fd);
// remove("myfifo");
return 0;
}
3. 注意事项
-
mkfifo错误处理:EEXIST错误应忽略,其他错误需处理
-
打开阻塞:open()会阻塞直到另一端也被打开
-
数据边界:需要约定数据格式,示例中以'\0'作为字符串结束
二、非阻塞I/O
1. 非阻塞读取端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_RDONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
int flag = fcntl(fd, F_GETFL);
//把管道的读写方式设置为非阻塞
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
flag = fcntl(fileno(stdin), F_GETFL);
//把标准输入stdin(0)的读写方式设置为非阻塞
fcntl(fileno(stdin), F_SETFL, flag | O_NONBLOCK);
while (1)
{
char buf[1024] = {0};
if (read(fd, buf, sizeof(buf)) >0)
{
printf("fifo_w:%s\n", buf);
}
if (fgets(buf, sizeof(buf), stdin))
{
printf("term:%s", buf);
fflush(stdout);
}
}
close(fd);
// remove("myfifo");
return 0;
}
2. 非阻塞写入端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[] = "hello,friend...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
3. 非阻塞I/O特点
-
fcntl设置 :使用
F_GETFL获取标志,F_SETFL设置O_NONBLOCK -
行为变化:
-
read()立即返回,不阻塞
-
如果无数据,read()返回-1,errno为EAGAIN
-
-
CPU使用:需要忙等待或轮询
三、信号驱动I/O
1. 信号驱动读取端
cpp
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int fd;
void myhandle(int num)
{
char buf[1024] = {0};
read(fd, buf, sizeof(buf));
printf("fifo_w:%s\n", buf);
}
int main(int argc, char **argv)
{
signal(SIGIO, myhandle);
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
fd = open("myfifo", O_RDONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
// 获得管道文件状态标志位
int flag = fcntl(fd, F_GETFL);
// 在原来标志的基础上 追加信号驱动
fcntl(fd, F_SETFL, flag | O_ASYNC); // 或受到 sigio 信号
// 设置sigio 信号的接收者
fcntl(fd,F_SETOWN,getpid());
while (1)
{
char buf[256]={0};
fgets(buf, sizeof(buf), stdin);
printf("term:%s", buf);
fflush(stdout);
}
close(fd);
// remove("myfifo");
return 0;
}
2. 信号驱动写入端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[] = "hello,friend...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
3. 信号驱动I/O特点
-
触发机制:当数据准备好时,内核发送SIGIO信号
-
设置步骤:
-
设置信号处理函数
-
使用
fcntl(fd, F_SETFL, flag | O_ASYNC)启用异步I/O -
使用
fcntl(fd, F_SETOWN, getpid())设置接收进程
-
-
优点:不需要轮询,CPU占用率低
四、select多路复用
1. select读取端 (04select_r.c)
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_RDONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
// 1 create set
fd_set rdset,tmpset; // read set
FD_ZERO(&rdset);
FD_ZERO(&tmpset);
// 2 . add fd to set
FD_SET(fd, &tmpset);
FD_SET(0, &tmpset);
while (1)
{
char buf[1024] = {0};
//5.clean flags
rdset = tmpset;
// 3 wait event
select(fd + 1, &rdset, NULL, NULL, NULL);
//4 . find fd
if (FD_ISSET(fd, &rdset)) //管道可以读取
{
read(fd, buf, sizeof(buf));
printf("fifo_w:%s\n", buf);
}
if (FD_ISSET(0, &rdset)) // stdin 可以读取
{
fgets(buf, sizeof(buf), stdin);
printf("term:%s", buf);
fflush(stdout);
}
}
close(fd);
// remove("myfifo");
return 0;
}
2. select写入端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[] = "hello,friend...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
3. select使用步骤
-
创建fd_set :使用
FD_ZERO初始化 -
添加文件描述符 :使用
FD_SET添加需要监控的fd -
调用select:阻塞等待事件发生
-
检查结果 :使用
FD_ISSET检查哪些fd就绪 -
重置fd_set:select会修改fd_set,需要重新设置
五、epoll多路复用
1. epoll读取端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int add_fd(int epfd, int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN; // 关心读事件
ev.data.fd = fd; // 方便后期查找 fd
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
if (-1 == ret)
{
perror("add_fd");
return ret;
}
return 0;
}
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_RDONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
// 1 create set
struct epoll_event rev[2];
int epfd = epoll_create(2);
if (-1 == epfd)
{
perror("epoll_create");
return 1;
}
// 2 add fd
add_fd(epfd, 0);
add_fd(epfd, fd);
while (1)
{
char buf[1024] = {0};
// 3 wait event
int ep_ret = epoll_wait(epfd, rev, 2, -1);
//4. 找到对应的文件描述符,并处理
for (int i = 0; i < ep_ret; i++)
{
if (rev[i].data.fd == fd)
{
read(fd, buf, sizeof(buf));
printf("fifo_w:%s\n", buf);
}
if (0 == rev[i].data.fd)
{
fgets(buf, sizeof(buf), stdin);
printf("term:%s", buf);
fflush(stdout);
}
}
}
close(fd);
// remove("myfifo");
return 0;
}
2. epoll写入端
cpp
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果是文件已存在的错误,程序就继续运行
if (EEXIST == errno)
{
}
else //如果是其他的错误,进程结束
{
perror("mkfifo");
return 1;
}
}
// open 会阻塞,
//写段先运行 ,写段会等读段出现,
// 读段先运行 ,读段会等写段出现,
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[] = "hello,friend...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
3. epoll使用步骤
-
创建epoll实例 :
epoll_create() -
注册事件 :使用
epoll_ctl(EPOLL_CTL_ADD)添加监控的fd -
等待事件 :
epoll_wait()阻塞等待事件 -
处理事件:遍历返回的事件数组进行处理
4. epoll vs select
| 特性 | select | epoll |
|---|---|---|
| 最大fd数 | 受FD_SETSIZE限制(通常1024) | 仅受系统资源限制 |
| 效率 | O(n)线性扫描 | O(1)事件通知 |
| 内存复制 | 每次调用都需要复制fd_set | 内核与用户空间共享内存 |
| 触发模式 | 仅支持水平触发 | 支持水平触发和边缘触发 |
六、总结对比
I/O模型对比
| 模型 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 阻塞I/O | 同步等待数据 | 编程简单 | 效率低,无法处理多路 | 简单应用 |
| 非阻塞I/O | 轮询检查状态 | 不会阻塞进程 | CPU占用高 | 低延迟应用 |
| 信号驱动 | 信号通知就绪 | 异步处理,CPU占用低 | 编程复杂,信号可能丢失 | 低负载应用 |
| select | 多路复用轮询 | 可监控多个fd | fd数有限,效率随fd增加下降 | 跨平台,fd数少 |
| epoll | 事件通知机制 | 高效,支持大量fd | Linux特有 | 高性能服务器 |
编程要点
-
FIFO创建:注意EEXIST错误处理
-
打开方式:O_RDONLY或O_WRONLY,会阻塞直到另一端打开
-
数据边界:需要明确数据结束标志
-
资源清理:程序退出前关闭fd,可考虑删除FIFO文件
最佳实践建议
-
简单应用:使用阻塞I/O
-
多路监控:Linux下优先使用epoll,跨平台考虑select
-
高性能:epoll + 非阻塞I/O
-
信号安全:信号处理函数中避免复杂操作