了 fcntl
函数来操作文件描述符的状态标志,其中主要是为了设置非阻塞模式。下面是对 fcntl
函数及其参数的详细解释:
fcntl
函数
fcntl
是一个用于操作文件描述符的系统调用,可以用来设置或获取文件描述符的各种属性。其原型如下:
int fcntl(int fd, int cmd, ... /* arg */ );
fd
: 需要操作的文件描述符。cmd
: 操作类型(命令),指定要执行的操作。arg
: 额外参数,根据cmd
的不同可能需要。
参数解释
F_GETFL
和 F_SETFL
F_GETFL
: 获取文件描述符的当前状态标志。这个命令将返回文件描述符的当前状态标志。F_SETFL
: 设置文件描述符的状态标志。这个命令将文件描述符的状态标志设置为指定的值。
代码分析
int flag = fcntl(fd_r, F_GETFL);
fcntl(fd_r, F_SETFL, flag | O_NONBLOCK);
-
int flag = fcntl(fd_r, F_GETFL);
- 获取文件描述符
fd_r
的当前状态标志,并存储在变量flag
中。 F_GETFL
命令返回的状态标志包括文件描述符的访问模式(如读、写)和一些状态标志(如是否为非阻塞模式)。
- 获取文件描述符
-
fcntl(fd_r, F_SETFL, flag | O_NONBLOCK);
- 使用
F_SETFL
命令设置文件描述符fd_r
的状态标志。 flag | O_NONBLOCK
表示将O_NONBLOCK
标志添加到当前状态标志中。O_NONBLOCK
是一个标志,用于设置文件描述符为非阻塞模式。非阻塞模式意味着读写操作不会阻塞进程,如果无法立即完成操作,系统调用会立即返回,而不是等待。
- 使用
flag = fcntl(0, F_GETFL);
fcntl(0, F_SETFL, flag | O_NONBLOCK);
-
flag = fcntl(0, F_GETFL);
- 获取标准输入(文件描述符 0)的当前状态标志,并存储在变量
flag
中。
- 获取标准输入(文件描述符 0)的当前状态标志,并存储在变量
-
fcntl(0, F_SETFL, flag | O_NONBLOCK);
- 使用
F_SETFL
命令设置标准输入的状态标志。 flag | O_NONBLOCK
表示将O_NONBLOCK
标志添加到标准输入的当前状态标志中。- 这将把标准输入设置为非阻塞模式,使得读取标准输入的操作不会阻塞进程。
- 使用
总结
fcntl(fd_r, F_GETFL)
用于获取文件描述符fd_r
的当前状态标志。fcntl(fd_r, F_SETFL, flag | O_NONBLOCK)
用于将fd_r
的状态标志设置为非阻塞模式,确保对fd_r
的读写操作不会阻塞。- 同样,
fcntl(0, F_GETFL)
和fcntl(0, F_SETFL, flag | O_NONBLOCK)
将标准输入(文件描述符 0)设置为非阻塞模式,使得读取标准输入的操作也是非阻塞的。
通过这些设置,你可以确保读写操作在没有数据时不会导致程序阻塞,从而使程序能够在执行其他任务时保持响应。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
int fd_r = open("myfifo1",O_RDONLY);
if(-1 == fd_r)
{
perror("open");
return 1;
}
int flag = fcntl(fd_r,F_GETFL);
fcntl(fd_r,F_SETFL,flag|O_NONBLOCK);
flag = fcntl( 0,F_GETFL);
fcntl(0,F_SETFL,flag|O_NONBLOCK);
while(1)
{
char buf[128]={0};
if(read(fd_r,buf,sizeof(buf))>0)
{
printf("fifo:%s\n",buf);
}
bzero(buf,sizeof(buf));
if(fgets(buf,sizeof(buf),stdin))
{
printf("terminal:%s\n",buf);
}
}
return 0;
}
signal函数在多路io
演示了如何使用信号驱动 I/O(SIGIO
)来处理命名管道(FIFO)的异步事件。下面是对代码及各个函数的作用的详细解释:
代码解析
1. #include
和 全局变量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
int fd_r;
#include
:引入了必要的头文件,提供了函数原型和常量定义。fd_r
:定义一个全局变量,用于存储打开的 FIFO 文件描述符。
2. handle
函数
void handle(int num)
{
char buf[128]={0};
read(fd_r,buf,sizeof(buf));
printf("fifo:%s\n",buf);
return ;
}
handle
:信号处理函数,当收到SIGIO
信号时被调用。num
:传递给信号处理函数的信号编号,这里没有用到。read(fd_r, buf, sizeof(buf))
:从 FIFO 中读取数据到buf
。printf("fifo:%s\n", buf)
:打印读取到的数据。
3. main
函数
int main(int argc, char *argv[])
{
signal(SIGIO, handle);
signal(SIGIO, handle)
:设置信号处理函数,当进程接收到SIGIO
信号时,会调用handle
函数。
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
mkfifo("myfifo1", 0666)
:创建一个名为"myfifo1"
的 FIFO(命名管道)。如果文件已存在 (EEXIST
),则不处理错误。
fd_r = open("myfifo1", O_RDONLY);
if(-1 == fd_r)
{
perror("open");
return 1;
}
open("myfifo1", O_RDONLY)
:以只读模式打开 FIFO,并将文件描述符存储在fd_r
中。如果打开失败,打印错误信息并退出。
int flag = fcntl(fd_r, F_GETFL);
fcntl(fd_r, F_SETFL, flag | O_ASYNC);
fcntl(fd_r, F_GETFL)
:获取当前文件描述符的状态标志。fcntl(fd_r, F_SETFL, flag | O_ASYNC)
:设置文件描述符为异步模式 (O_ASYNC
)。这意味着当文件描述符有数据可读时,会向进程发送SIGIO
信号。
fcntl(fd_r, F_SETOWN, getpid());
fcntl(fd_r, F_SETOWN, getpid())
:设置fd_r
文件描述符的信号所有者为当前进程(getpid()
)。这使得SIGIO
信号会发送到当前进程。
while(1)
{
char buf[128]={0};
bzero(buf, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
printf("terminal:%s\n", buf);
}
return 0;
}
- 无限循环:
fgets(buf, sizeof(buf), stdin)
:从标准输入读取数据到buf
。printf("terminal:%s\n", buf)
:打印从标准输入读取的数据。
主要函数的作用
-
signal(SIGIO, handle)
:- 用于设置进程在接收到
SIGIO
信号时调用handle
函数。
- 用于设置进程在接收到
-
mkfifo("myfifo1", 0666)
:- 创建一个命名管道(FIFO)。如果已存在,则不处理错误。
-
open("myfifo1", O_RDONLY)
:- 打开创建的 FIFO 文件,以只读模式获取文件描述符。
-
fcntl(fd_r, F_GETFL)
和fcntl(fd_r, F_SETFL, flag | O_ASYNC)
:- 获取和设置文件描述符的状态标志,使其处于异步模式。异步模式会使文件描述符触发信号(
SIGIO
),当数据到达时通知进程。
- 获取和设置文件描述符的状态标志,使其处于异步模式。异步模式会使文件描述符触发信号(
-
fcntl(fd_r, F_SETOWN, getpid())
:- 设置文件描述符
fd_r
的信号所有者为当前进程,使得SIGIO
信号会发送到当前进程。
- 设置文件描述符
-
read(fd_r, buf, sizeof(buf))
:- 在信号处理函数中,从 FIFO 中读取数据。
-
fgets(buf, sizeof(buf), stdin)
和printf("terminal:%s\n", buf)
:- 在主循环中,读取标准输入的数据并打印。
总结
这段代码通过设置文件描述符 fd_r
为异步模式,并为其指定信号所有者,使得当有数据可读时会发送 SIGIO
信号。信号处理函数 handle
会在收到 SIGIO
时被调用,从 FIFO 中读取数据并打印。这种方式允许进程在等待数据的同时执行其他操作,比如读取标准输入。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
int fd_r;
void handle(int num)
{
char buf[128]={0};
read(fd_r,buf,sizeof(buf));
printf("fifo:%s\n",buf);
return ;
}
int main(int argc, char *argv[])
{
signal(SIGIO,handle);
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
fd_r = open("myfifo1",O_RDONLY);
if(-1 == fd_r)
{
perror("open");
return 1;
}
//给管道设置信号驱动
int flag = fcntl(fd_r,F_GETFL);
fcntl(fd_r,F_SETFL,flag| O_ASYNC);
//如果有写管道,本进程作为sigio信号的接收者
fcntl(fd_r,F_SETOWN,getpid());
while(1)
{
char buf[128]={0};
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("terminal:%s\n",buf);
}
return 0;
}
select
函数用于监视多个文件描述符的状态,以便在其中一个或多个文件描述符变为可读、可写或出现异常时进行处理。下面是对 select
相关函数和操作的详细解释。
select
函数
select
函数用于监视文件描述符集合,以确定哪些文件描述符在某个时间点是就绪的(即,可以进行读、写操作或出现异常)。其原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
: 文件描述符的数量+1。它通常是你感兴趣的最大文件描述符值加1。readfds
: 监视的文件描述符集合(用于读操作)。writefds
: 监视的文件描述符集合(用于写操作)。exceptfds
: 监视的文件描述符集合(用于异常条件)。timeout
: 指定select
函数的超时时间。如果超时时间为NULL
,则select
将无限期等待直到有文件描述符变为可用。
fd_set
操作函数
FD_ZERO(fd_set *set)
: 清空文件描述符集合set
。FD_SET(int fd, fd_set *set)
: 将文件描述符fd
添加到文件描述符集合set
中。FD_CLR(int fd, fd_set *set)
: 从文件描述符集合set
中移除文件描述符fd
。FD_ISSET(int fd, fd_set *set)
: 检查文件描述符fd
是否在文件描述符集合set
中。
代码分析
// 1. 创建文件描述符集合
fd_set rd_set, tmp_set; // 读集合
FD_ZERO(&rd_set); // 清空读集合
FD_ZERO(&tmp_set); // 清空临时集合
// 2. 添加文件描述符
FD_SET(0, &tmp_set); // 将标准输入(文件描述符 0)添加到临时集合
FD_SET(fd_r, &tmp_set); // 将文件描述符 fd_r 添加到临时集合
while (1) {
// 6. 复制临时集合到读集合
rd_set = tmp_set;
char buf[128] = {0};
// 3. 等待事件发生
select(fd_r + 1, &rd_set, NULL, NULL, NULL);
// 4. 查找发生事件的文件描述符
if (FD_ISSET(fd_r, &rd_set)) {
read(fd_r, buf, sizeof(buf));
printf("fifo: %s\n", buf);
}
if (FD_ISSET(0, &rd_set)) {
bzero(buf, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
printf("terminal: %s\n", buf);
}
}
-
创建文件描述符集合:
fd_set rd_set, tmp_set;
定义了两个fd_set
类型的变量,用于存储文件描述符集合。FD_ZERO(&rd_set);
清空rd_set
集合。FD_ZERO(&tmp_set);
清空tmp_set
集合。
-
添加文件描述符:
FD_SET(0, &tmp_set);
将标准输入(文件描述符 0)添加到tmp_set
集合中。FD_SET(fd_r, &tmp_set);
将文件描述符fd_r
添加到tmp_set
集合中。
-
等待事件:
rd_set = tmp_set;
复制tmp_set
到rd_set
,rd_set
用于select
调用,避免在select
调用后丢失对文件描述符的跟踪。select(fd_r + 1, &rd_set, NULL, NULL, NULL);
等待直到fd_r
或标准输入有数据可读。fd_r + 1
是需要监视的文件描述符的数量加1。
-
处理事件:
FD_ISSET(fd_r, &rd_set)
检查fd_r
是否在rd_set
中,即检查fd_r
是否有数据可读。如果有,读取数据并打印。FD_ISSET(0, &rd_set)
检查标准输入是否在rd_set
中,即检查标准输入是否有数据可读。如果有,读取数据并打印。
总结
select
用于监视多个文件描述符,以确定哪些文件描述符准备好进行读、写或异常处理。fd_set
操作函数用于创建和修改文件描述符集合。- 在代码中,
select
被用于等待标准输入或文件描述符fd_r
中有数据可读,并根据文件描述符的状态执行相应的读取操作。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
int fd_r = open("myfifo1",O_RDONLY);
if(-1 == fd_r)
{
perror("open");
return 1;
}
//1 create set
fd_set rd_set,tmp_set; //read set
FD_ZERO(&rd_set);
FD_ZERO(&tmp_set);
// 2. add fd
FD_SET(0,&tmp_set);
FD_SET(fd_r,&tmp_set);
while(1)
{
//6.clean flag
rd_set = tmp_set;
char buf[128]={0};
//3 wait event
select(fd_r+1,&rd_set,NULL,NULL,NULL );
//4 找到对应的fd
if(FD_ISSET(fd_r,&rd_set))
{
read(fd_r,buf,sizeof(buf));
printf("fifo:%s\n",buf);
}
if(FD_ISSET(0,&rd_set))
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("terminal:%s\n",buf);
}
}
return 0;
}
epoll
epoll
被用来实现对两个文件描述符的事件通知机制。下面是对各个函数和操作的详细分析:
1. add_fd(int epfd, int fd)
这个函数用于将一个文件描述符 fd
添加到 epoll
实例 epfd
中,并指定感兴趣的事件类型。
- 参数:
epfd
:epoll_create
创建的epoll
文件描述符。fd
:要添加到epoll
的文件描述符。
- 操作:
- 创建一个
epoll_event
结构体ev
,设置感兴趣的事件为EPOLLIN
,表示关注该文件描述符的可读事件。 - 使用
epoll_ctl
函数将fd
添加到epoll
实例中,操作类型为EPOLL_CTL_ADD
。
- 创建一个
- 返回值:
- 返回
epoll_ctl
的结果,如果返回-1
表示添加失败,并调用perror
打印错误信息。
- 返回
2. main(int argc, char *argv[])
主函数的执行流程:
-
创建命名管道(FIFO):
- 使用
mkfifo
创建两个命名管道"myfifo1"
和"myfifo2"
,这两个管道用于进程间通信。如果创建失败并且错误码不是EEXIST
(文件已存在),则打印错误信息并退出。
- 使用
-
打开命名管道:
- 使用
open
打开myfifo1
(以只读模式)和myfifo2
(以只写模式),分别获得fd_r
和fd_w
文件描述符。如果打开失败,打印错误信息并退出。
- 使用
-
创建
epoll
实例:- 使用
epoll_create
创建一个epoll
实例,并返回一个文件描述符epfd
。如果创建失败,打印错误信息并退出。
- 使用
-
添加文件描述符到
epoll
实例:- 调用
add_fd
函数将标准输入(文件描述符0
)和管道myfifo1
的文件描述符fd_r
添加到epoll
实例中,开始监听它们的事件。
- 调用
-
事件监听循环:
-
进入无限循环,调用
epoll_wait
等待事件发生。epoll_wait
会阻塞直到有事件发生,或者超时(这里设置为-1
,表示无限等待)。 -
事件处理:
- 如果
epoll_wait
返回有事件发生,遍历所有的事件:- 如果事件的文件描述符是标准输入(
0
),读取输入数据并写入到fd_w
,如果输入为"#quit\n"
,则退出程序。 - 如果事件的文件描述符是
fd_r
,从fd_r
读取数据并打印。如果读取的数据是"#quit\n"
或读取失败,则退出程序。
- 如果事件的文件描述符是标准输入(
- 如果
-
主要函数和操作的作用总结
-
epoll_create
:创建epoll
实例,返回epoll
文件描述符。此文件描述符用于后续的epoll_ctl
和epoll_wait
操作。 -
epoll_ctl
:对epoll
实例进行控制操作,如添加、修改或删除文件描述符及其事件。此处用于将文件描述符添加到epoll
实例中。 -
epoll_wait
:等待epoll
实例中的文件描述符发生事件。阻塞直到有文件描述符的事件发生或者超时,返回发生的事件数量。 -
fgets
和write
:用于从标准输入读取数据,并写入到fd_w
对应的管道中。 -
read
和printf
:用于从fd_r
对应的管道中读取数据,并输出到标准输出。
这段代码展示了如何使用 epoll
机制来处理异步 I/O 操作,同时实现了基于命名管道的进程间通信。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/epoll.h>
int add_fd(int epfd,int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev);
if(-1==ret)
{
perror("add_fd");
}
return ret;
}
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
ret = mkfifo("myfifo2",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
int fd_r = open("myfifo1",O_RDONLY);
if(-1 == fd_r)
{
perror("open");
return 1;
}
int fd_w = open("myfifo2",O_WRONLY);
if(-1 == fd_w)
{
perror("open");
return 1;
}
struct epoll_event rev[2];
int epfd;
int ep_ret;
epfd = epoll_create(2);
if(-1==epfd)
{
perror("epoll_create");
return 1;
}
add_fd(epfd,0);
add_fd(epfd,fd_r);
while(1)
{
ep_ret = epoll_wait(epfd,rev,2,-1);
if(ep_ret>0)
{
int i=0;
for(i=0;i<ep_ret;i++)
{
if(0==rev[i].data.fd)
{
printf("to A:");
char buf[128]={0};
fgets(buf,sizeof(buf),stdin);//#quit
if(0 == strcmp(buf,"#quit\n"))
{
exit(0);
}
write(fd_w,buf,strlen(buf));
}
else if(fd_r==rev[i].data.fd)
{
char buf[128]={0};
int ret = read(fd_r,buf,sizeof(buf));
if(0==strcmp(buf,"#quit\n") || ret<=0)
{
exit(0);
}
printf("from A:%s",buf);
fflush(stdout);
}
}
}
}
return 0;
}
select和epoll函数区别
select
函数和 epoll
函数都是用于处理多路复用的系统调用,但它们在性能、功能和使用场景上有一些关键的区别:
1. 适用平台
select
:是一个在多个操作系统中都可用的标准系统调用,包括 UNIX、Linux、Windows 等。epoll
:是 Linux 特有的系统调用,其他操作系统不支持。
2. 文件描述符数量限制
select
:有一个文件描述符数量的限制,通常由FD_SETSIZE
宏定义,默认值是 1024(可以通过编译时选项修改,但系统限制较多)。epoll
:没有固定的文件描述符数量限制,理论上可以处理大量文件描述符,适合高并发场景。
3. 性能
select
:每次调用select
时,都会将所有需要监控的文件描述符集合传递到内核,这导致了性能问题,尤其是在文件描述符数量很大的时候。epoll
:采用事件通知机制,只在文件描述符状态发生变化时才通知应用程序,避免了不必要的轮询,性能更高。
4. 内存使用
select
:每次调用都需要复制和修改文件描述符集合,内存消耗相对较大。epoll
:使用epoll_ctl
进行文件描述符的添加、修改和删除操作,内存消耗较少。
5. 编程接口
select
:需要传递三个文件描述符集合(读、写、异常),并在每次调用时重新设置这些集合。epoll
:使用epoll_ctl
来管理文件描述符,并通过epoll_wait
获取事件,操作相对简单。
6. 支持的触发模式
select
:只支持水平触发(Level-Triggered, LT),即每次调用select
都会返回当前有事件的文件描述符。epoll
:支持水平触发和边缘触发(Edge-Triggered, ET),边缘触发模式在事件发生时会通知一次,适用于高效处理稀疏的事件。
7. 事件通知
select
:需要轮询所有的文件描述符,耗时较长。epoll
:只通知那些发生事件的文件描述符,避免了不必要的检查。
总结
select
适合文件描述符数量少、系统要求兼容性强的情况。epoll
适合需要处理大量文件描述符、高并发的高性能应用。
如果你的应用程序需要处理大量并发连接,epoll
通常是更好的选择。如果是较小规模的应用,select
可能已经足够。
第一步 创建epoll_create
第二步 添加成员
第三步 epoll_wait()
有timeout的使用