为什么引入多路转接:
在前面 状态机实现 拷贝两个设备之间的数据的程序中,可以发现这样的问题:程序在运行期间,一定是一个忙等的状态,CPU 使用率很高,很浪费CPU时间,原因在于程序其实大部分时间都在忙于 判断假错,重读 重写的状态中循环。这个属于IO 密集,负载不密集的任务,即数据量不大,但是IO 很密集。对于IO密集型的任务 可以对程序进行IO多路转接,本质就是监视文件描述符的行为,当 当前文件描述符发生了我感兴趣的行为的时候,才会去做后续操作,如前面 两个设备之间的数据交换,前面的做法是盲推,一直不停的探测,尝试,看有没有内容可读或者可写。如果我们用IO 多路转接,就可以变成 当某个文件描述符状态发生了我感兴趣的动作的时候,我才会做后续操作,节省CPU时间。
IO 多路转接: 监视文件描述符的行为,fd可读?可写?有异常条件待处理?当文件描述符达到了我们感兴趣的状态的时候,函数返回。
select(),可移植
poll(),可移植
epoll(),Linux 基于 poll 的方言
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
SYNOPSIS
/* 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>
该函数 如果不实现超时设置,那么该函数会阻塞死等,一直等到感兴趣的事件发生,即关心的可读,可写的文件描述符集合中 文件描述符变成 可读,可写状态的时候,该函数才会返回。注意三个 fd_set 不是const类型,是可变的。当select()返回的时候,这三个集合当中就不再是我们之前所布置的监事现场了,而是存放结果的场所。如果select()不设置超时时间 阻塞等待 被信号打断,发生了假错,那么这三个集合也将被清空。
nfds : 文件描述符数量,不过指的是 所包含的最大文件描述符 +1 的值
readfds:可读的 文件描述符集合
writefds:可写的 文件描述符集合
exceptfds:异常的文件描述符集合
timeout 超时设置,阻塞。如果不实现超时设置,那么该函数会死等
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);//从指定的文件描述符集合中删除指定文件描述符fd
int FD_ISSET(int fd, fd_set *set);//判断文件描述符fd 是否存在于文件描述符集合set中
void FD_SET(int fd, fd_set *set);// 将文件描述符fd 放到 文件描述符集合set中
void FD_ZERO(fd_set *set); //清空一个文件描述符集合
RETURN VALUE
返回值是 现在发生了我们感兴趣事件的文件描述符行为的个数,并且依然将这些文件描述符放在 读集,写集,错误集 这是哪个文件描述符集合中。
失败返回-1,设置errno。EINTR 属于假错,阻塞等待 可以被信号打断
ERRORS
EINTR A signal was caught; see signal(7). 阻塞等待 可以被信号打断
EINVAL nfds is negative or exceeds the RLIMIT_NOFILE resource limit (see getrlimit(2)).
EINVAL the value contained within timeout is invalid.
ENOMEM unable to allocate memory for internal tables.
The time structures involved are defined in <sys/time.h> and look like
秒+微秒
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
注意 经常用 select(NULL,NULL,NULL,NULL, 设置超时时间) 来实现一个安全的休眠。
实验1:改进 状态机实现 用多路转接IO 拷贝两个设备之间的数据
程序就不再是一直处于 假错 重读 重写 中循环,cpu使用率大大降低,因为大多数时间都阻塞在 select()等待 文件描述符变化。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#define TTY1 "/dev/tty11"
#define TTY2 "/dev/tty12"
#define BUFSIZE 1024
enum
{
STATE_R = 1,
STATE_W,
STATE_AUTO,
STATE_Ex,
STATE_T
};
struct fsm_st
{
int state;
int sfd;
int dfd;
char buf[BUFSIZE];
int len;
int pos;
char* errstr;
};
static void fsm_driver(struct fsm_st *fsm)
{
int ret;
switch(fsm->state)
{
case STATE_R:
fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE);
if(fsm->len == 0)
fsm->state = STATE_T;
else if(fsm->len < 0)
{
if(errno == EAGAIN)
fsm->state = STATE_R;
else
{
fsm->errstr = "read()";
fsm->state = STATE_Ex;
}
}
else
{
fsm->pos = 0;
fsm->state = STATE_W;
}
break;
case STATE_W:
ret = write(fsm->dfd,fsm->buf+fsm->pos,fsm->len);
if(ret < 0)
{
if(errno == EAGAIN)
fsm->state = STATE_W;
else
{
fsm->errstr = "read()";
fsm->state = STATE_Ex;
}
}
else
{
fsm->pos += ret;
fsm->len -= ret;
if(fsm->len == 0)
fsm->state = STATE_R;
else
fsm->state = STATE_W;
}
break;
case STATE_Ex:
perror(fsm->errstr);
fsm->state = STATE_T;
break;
case STATE_T:
/* do something*/
break;
default:
abort();
break;
}
}
static int max(int a,int b)
{
if(a > b)
{
return a;
}
return b;
}
static void relay(int fd1,int fd2)
{
int fd1_save,fd2_save;
struct fsm_st fsm12,fsm21;
fd_set rset,wset;
fd1_save = fcntl(fd1,F_GETFL);
fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK);
fd2_save = fcntl(fd2,F_GETFL);
fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK);
fsm12.state = STATE_R;
fsm12.sfd = fd1;
fsm12.dfd = fd2;
fsm21.state = STATE_W;
fsm21.sfd = fd2;
fsm21.dfd = fd1;
while(fsm12.state != STATE_T || fsm21.state != STATE_T)
{
//设置现场
FD_ZERO(&rset);
FD_ZERO(&wset);
if(fsm12.state == STATE_R)
FD_SET(fsm12.sfd,&rset);
if(fsm12.state == STATE_W)
FD_SET(fsm12.dfd,&wset);
if(fsm21.state == STATE_R)
FD_SET(fsm21.sfd,&rset);
if(fsm21.state == STATE_W)
FD_SET(fsm21.dfd,&wset);
//监控
if(fsm12.state < STATE_AUTO || fsm21.state < STATE_AUTO)
{
if(select(max(fd1,fd2)+1,&rset,&wset,NULL,NULL) < 0)
{
if(errno == EINTR)
continue;
perror("select()");
exit(1);
}
}
//根据监控结果 最下一步动作
if(FD_ISSET(fd1,&rset) || FD_ISSET(fd2,&wset) || fsm12.state > STATE_AUTO)
fsm_driver(&fsm12);
if(FD_ISSET(fd2,&rset) || FD_ISSET(fd1,&wset) || fsm12.state > STATE_AUTO)
fsm_driver(&fsm21);
}
fcntl(fd1,F_SETFL,fd1_save);
fcntl(fd2,F_SETFL,fd2_save);
}
int main()
{
int fd1,fd2;
fd1 = open(TTY1,O_RDWR);
if(fd1 < 0)
{
perror("open()");
exit(1);
}
fd2 = open(TTY2,O_RDWR|O_NONBLOCK);
if(fd1 < 0)
{
perror("open()");
exit(1);
}
relay(fd1,fd2);
close(fd2);
close(fd1);
}
select()的问题:
1 监视现场位置(三个集合) 和 监视结果位置(三个集合) 相同,用的是同一块空间,如果 读集合,写集合,出错集合 各设置10个文件描述符,一共三十个文件描述符,只要任意一个文件描述符发生变化,如读集中某一个文件描述符 变成了可读状态,则函数返回,即返回读集合中能读的文件描述符个数,即1。而其他集合就会被清空。再次监控,需要重新设置监视现场。
2 一个进程中能够打开的文件描述符地方个数是可以更改的,ulimit -a。 那么第一个参数 nfds就有问题,如果我们监视的文件描述符个数 超过了 有符号整形最大值范围,则会溢出。