Linux系统编程58 高级IO - IO 多路转接之 select() 监视文件描述符读,写,异常状态

为什么引入多路转接:
在前面 状态机实现 拷贝两个设备之间的数据的程序中,可以发现这样的问题:程序在运行期间,一定是一个忙等的状态,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()的问题:

扫描二维码关注公众号,回复: 12940093 查看本文章

1 监视现场位置(三个集合) 和 监视结果位置(三个集合) 相同,用的是同一块空间,如果 读集合,写集合,出错集合 各设置10个文件描述符,一共三十个文件描述符,只要任意一个文件描述符发生变化,如读集中某一个文件描述符 变成了可读状态,则函数返回,即返回读集合中能读的文件描述符个数,即1。而其他集合就会被清空。再次监控,需要重新设置监视现场。

2 一个进程中能够打开的文件描述符地方个数是可以更改的,ulimit -a。 那么第一个参数 nfds就有问题,如果我们监视的文件描述符个数 超过了 有符号整形最大值范围,则会溢出。

select()是以事件为单位 组织文件描述符

猜你喜欢

转载自blog.csdn.net/LinuxArmbiggod/article/details/114670289