030 UNIX再学习 -- 函数 select、poll、epoll
程序员文章站
2022-05-02 15:11:55
...
这部分是相当重要的一部分,之前在工作项目中有用到过,特意认真的看过。文章最后会把我项目用到部分的源码贴出。
再有值得纪念的一下,原创文章数终于赶上转载文章数了。说明我找到的学习方法是对了,一开始茫然不知所措时,学习是做加法,将看到的好的文章转载;但到了一定阶段,要懂得做减法了,将之前转载部分系统的分类总结;最后就是举一反三,将难题,转化成自己熟悉的问题解决。
文章开头总是很难的,要讲的东西太多,不知道从哪下笔。我就以下面参看这篇文章为基础开讲:
参看:select、poll、epoll之间的区别总结[整理]
一、相关概念
select、poll、epoll 都是 I/O 多路复用的机制。I/O 多路复用机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select、poll、epoll本质上都是同步 I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责吧数据从内核拷贝到用户空间。
上面这段文字简单介绍了一下我们要讲到的这三个函数。其中有几个概念需要搞清楚。
(1)I/O 多路复用机制
I/O 多路复用,也称为 I/O 多路转接。APUE 上只有这样一句话。
为了使用这种技术,先构建一张我们感兴趣的描述符(通常都不止一个)的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行 I/O 时,该函数才返回。poll、pselect 和 select 这 3 个函数使我们能够执行 I/O 多路转接。在从这些函数返回时,进程会被告知哪些描述符已准备好可以进行 I/O。
这是什么鬼,还是没有理解多路转接到底是怎么回事,然后我各种百度(谷歌虽好但要*)也没查到合适的解释。
I/O 多路转接,英文是 I/O multiplexing,最后在知乎上看到还比较容易理解的解释。
I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Socket (I/O流)的状态(对应空管塔里面的 Fight progress strip槽)来同时管理多个 I/O 流。 发明它的原因,是尽量多的提高服务器的吞吐能力。
(2)同步 I/O
稍后再讲
(3)异步 I/O
稍后再讲
(4)阻塞 I/O 与非阻塞 I/O
比如 read 和 write,通常 IO 操作都是阻塞 I/O 的,也就是说当你调用 read 时,如果没有数据收到,那么线程或者进程就会被挂起,直到收到数据。
这样,当服务器需要处理1000个连接的的时候,而且只有很少连接忙碌的,那么会需要1000个线程或进程来处理1000个连接,而1000个线程大部分是被阻塞起来的。由于CPU的核数或超线程数一般都不大,比如4,8,16,32,64,128,比如4个核要跑1000个线程,那么每个线程的时间槽非常短,而线程切换非常频繁。这样是有问题的:
1、线程是有内存开销的,1个线程可能需要512K(或2M)存放栈,那么1000个线程就要512M(或2G)内存。
2、线程的切换,或者说上下文切换是有CPU开销的,当大量时间花在上下文切换的时候,分配给真正的操作的CPU就要少很多。
那么,我们就要引入非阻塞I/O的概念,非阻塞IO很简单,通过 fcntl(POSIX)或 ioctl(Unix)设为非阻塞模式,这时,当你调用 read 时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,如EWOULDBLOCK。这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入。
于是,我们需要引入IO多路复用的概念。多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。
使用select函数的方式如下图所示:
使用select函数的方式如下图所示:
二、函数 select
-
/* According to POSIX.1-2001 */
-
#include <sys/select.h>
-
-
/* According to earlier standards */
-
#include <sys/time.h>
-
#include <sys/types.h>
-
#include <unistd.h>
-
-
int select(int nfds, fd_set *readfds, fd_set *writefds,
-
fd_set *exceptfds, struct timeval *timeout);
-
返回值:准备就绪的描述符数目:若超时,返回 0;若出错,返回 -1
1、参数解析
(1)参数 timeout,它指定愿意等待的时间长度,单位为秒和微妙。
时间函数,我们讲了好多次了。参看:C语言再学习 -- 时间函数
结构体 timeval 定义如下:
-
struct timeval {
-
time_t tv_sec; /* seconds */
-
suseconds_t tv_usec; /* microseconds */
-
};
timeout 的取值有以下 3 中情况:
timeout == NULL
永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则 select 返回 -1,errno 设置为 EINTR。
timeout->tv_sec == 0 && timeout->tv_usec == 0
timeout->tv_sec == 0 && timeout->tv_usec == 0
根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞 select 函数的方法。
timeout->tv_sec != 0 || timeout->tv_usec != 0
等待指定的秒数和微妙数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超时到期时还没有一个描述符准备好,则返回值是 0。(如果系统不提供微秒级的精度,则 timeout->tv_usec 值取整到最近的支持值)与第一种情况一样,这种等待可被捕捉到的信号中断。
(2)中间 3 个参数 readfds(读)、writefds(写) 和 exceptfds(异常) 是指向描述符集的指针。
这 3 个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个 fd_set 数据类型中。这个数据类型是由实现选择的,它可以为每一个可能的描述符保持以为。我们可以认为它只是一个很大的字节数组。
重点来了,对于 fd_set 数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型的变量使用以下 4 个函数中的一个。
-
void FD_CLR(int fd, fd_set *set); //
-
int FD_ISSET(int fd, fd_set *set);
-
void FD_SET(int fd, fd_set *set);
-
void FD_ZERO(fd_set *set);
这些接口可实现为宏或函数。调用 FD_ZERO 将一个 fd_set 变量的所有位设置为 0。要开启描述符集中的一位,可以调用 FD_SET。调用 FD_CLR 可以清除一位。最后,可以调用 FD_ISSET 测试描述符集中的一个指定位是否已打开。
在声明了一个描述符集之后,必须用 FD_ZERO 将这个描述符集置为 0,然后在其中设置我们关心的各个描述符的位。具体操作如下所示:
-
fd_set rset;
-
int fd;
-
FD_ZERO (&rset);
-
FD_SET (fd, &rset);
-
FD_SET (STDIN_FILEND, &rset);
从 select 返回时,可以用 FD_ISSET 测试该集中的一个给定位是否仍处于打开状态。
-
if (FD_ISSET (fd, &rset))
-
{
-
....
-
}
select 的中间 3 个参数(指向描述符集的指针)中的任意一个(或全部)可以是空指针,这表示对相应条件并不关心。如果所有 3 个指针都是 NULL,则 select 提供了比 sleep 更精确的定时器。(3)select 第一个参数 nfds 的意思是“最大文件描述符编号值加 1”
考虑所有 3 个描述符集,在 3 个描述符集中找出最大描述符编号值,然后加 1,这就是第一个参数值。
也可将第一个参数设置为 FD_SETSIZE,这是 <sys/select.h> 中的一个常量,它指定最大描述符数(经常是 1024),但是对大多数应用程序而言,此值太大了。
-
aaa@qq.com:/usr/include# grep "FD_SETSIZE" * -rn
-
i386-linux-gnu/sys/select.h:79:#define FD_SETSIZE __FD_SETSIZE
-
i386-linux-gnu/bits/typesizes.h:63:#define __FD_SETSIZE 1024
2、返回值
select 有 3 个可能的返回值。(1)返回值 -1表示出错。
这是可能发生的,例如,在所指定的描述符一个都没准备好时捕捉到一个信号。在此情况下,一个描述符集都不修改。
错误码有:
-
EBADF An invalid file descriptor was given in one of the sets. (Perhaps a file descriptor that was already closed, or one on which
-
an error has occurred.)
-
-
EINTR A signal was caught; see signal(7).
-
-
EINVAL nfds is negative or the value contained within timeout is invalid.
-
-
ENOMEM unable to allocate memory for internal tables.
(2)返回值 0 表示没有描述符准备好。
若指定的描述符一个都没准备好,指定的时间就过去了,那么就会发生这种情况。此时,所有描述符集都会置 0.
(3)一个正返回值说明了已经准备好的描述符数。
该值是 3 个描述符集中已准备好的描述符数之和,所以如果同一描述符已准备好读和写,那么在返回值中会对其计两次数。在这种情况下,3 个描述符集中仍旧打开对的位对应于已准备好的描述符。
对于“准备好”的含义要作一些更具体的说明。
若对读集(readfds)中的一个描述符进行的 read 操作不会阻塞,则认为此描述符是准备好的。
若对写集(writefds)中的一个描述符进行的 write 操作不会阻塞,则认为此描述符是准备好的。
若对异常条件集(exceptfds)中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。(现在,异常条件包括:网络链接上到达带外的数据,或者在处于数据包模式的伪终端上发生了某些条件)。
对于读、写和异常条件,普通文件的文件描述符总是返回准备好。
3、示例说明
-
示例一:用来循环读取键盘输入
-
#include <stdio.h>
-
#include <sys/select.h>
-
#include <time.h>
-
#include <sys/types.h>
-
#include <unistd.h>
-
#include <sys/stat.h>
-
#include <fcntl.h>
-
#include <assert.h>
-
-
int main (void)
-
{
-
int keyboard;
-
int ret, i;
-
char c;
-
fd_set readfd;
-
struct timeval timeout;
-
//打开 /dev/tty 只读非阻塞
-
keyboard = open ("/dev/tty", O_RDONLY | O_NONBLOCK);
-
//assert 断言宏
-
assert (keyboard > 0);
-
-
while (1)
-
{
-
timeout.tv_sec = 5;
-
timeout.tv_usec = 0;
-
FD_ZERO (&readfd);
-
FD_SET (keyboard, &readfd);
-
ret = select (keyboard + 1, &readfd, NULL, NULL, &timeout);
-
//select error when ret = -1
-
if (ret == -1)
-
perror ("select error");
-
-
//data coming when ret>0
-
else if (ret)
-
{
-
if (FD_ISSET (keyboard, &readfd))
-
{
-
i = read (keyboard, &c, 1);
-
if ('\n' == c)
-
continue;
-
printf ("the input is %c\n", c);
-
-
if ('q' == c)
-
break;
-
}
-
}
-
//time out when ret = 0
-
else if (ret == 0)
-
printf ("time out\n");
-
}
-
}
-
-
输出结果:
-
(5秒内操作的话)
-
s
-
the input is s
-
d
-
the input is d
-
f
-
the input is f
-
q
-
the input is q
-
(结束)
-
-
(5秒内不操作的话)
-
time out
-
time out
-
time out
-
time out
-
//示例二:通过select系统调用进行io多路切换,实现异步读取串口数据
-
#include<stdio.h>
-
#include<stdlib.h>
-
#include<unistd.h>
-
#include<sys/types.h>
-
#include<sys/stat.h>
-
#include<sys/signal.h>
-
#include<fcntl.h>
-
#include<termios.h>
-
#include<errno.h>
-
#include <string.h>
-
-
#define FALSE -1
-
#define TRUE 0
-
#define flag 1
-
#define noflag 0
-
-
int wait_flag = noflag;
-
int STOP = 0;
-
int res;
-
-
int speed_arr[] =
-
{ B38400, B19200, B9600, B4800, B2400, B1200, B300, B38400, B19200, B9600,
-
B4800, B2400, B1200, B300, };
-
int name_arr[] =
-
{ 38400, 19200, 9600, 4800, 2400, 1200, 300, 38400, 19200, 9600, 4800, 2400,
-
1200, 300, };
-
void
-
set_speed (int fd, int speed)
-
{
-
int i;
-
int status;
-
struct termios Opt;
-
tcgetattr (fd, &Opt);
-
for (i = 0; i < sizeof (speed_arr) / sizeof (int); i++)
-
{
-
if (speed == name_arr[i])
-
{
-
tcflush (fd, TCIOFLUSH);
-
cfsetispeed (&Opt, speed_arr[i]);
-
cfsetospeed (&Opt, speed_arr[i]);
-
status = tcsetattr (fd, TCSANOW, &Opt);
-
if (status != 0)
-
{
-
perror ("tcsetattr fd1");
-
return;
-
}
-
tcflush (fd, TCIOFLUSH);
-
}
-
}
-
}
-
-
int
-
set_Parity (int fd, int databits, int stopbits, int parity)
-
{
-
struct termios options;
-
if (tcgetattr (fd, &options) != 0)
-
{
-
perror ("SetupSerial 1");
-
return (FALSE);
-
}
-
options.c_cflag &= ~CSIZE;
-
switch (databits)
-
{
-
case 7:
-
options.c_cflag |= CS7;
-
break;
-
case 8:
-
options.c_cflag |= CS8;
-
break;
-
default:
-
fprintf (stderr, "Unsupported data size\n");
-
return (FALSE);
-
}
-
switch (parity)
-
{
-
case 'n':
-
case 'N':
-
options.c_cflag &= ~PARENB; /* Clear parity enable */
-
options.c_iflag &= ~INPCK; /* Enable parity checking */
-
break;
-
case 'o':
-
case 'O':
-
options.c_cflag |= (PARODD | PARENB);
-
options.c_iflag |= INPCK; /* Disnable parity checking */
-
break;
-
case 'e':
-
case 'E':
-
options.c_cflag |= PARENB; /* Enable parity */
-
options.c_cflag &= ~PARODD;
-
options.c_iflag |= INPCK; /* Disnable parity checking */
-
break;
-
case 'S':
-
case 's': /*as no parity */
-
options.c_cflag &= ~PARENB;
-
options.c_cflag &= ~CSTOPB;
-
break;
-
default:
-
fprintf (stderr, "Unsupported parity\n");
-
return (FALSE);
-
}
-
-
switch (stopbits)
-
{
-
case 1:
-
options.c_cflag &= ~CSTOPB;
-
break;
-
case 2:
-
options.c_cflag |= CSTOPB;
-
break;
-
default:
-
fprintf (stderr, "Unsupported stop bits\n");
-
return (FALSE);
-
}
-
/* Set input parity option */
-
if (parity != 'n')
-
options.c_iflag |= INPCK;
-
tcflush (fd, TCIFLUSH);
-
options.c_cc[VTIME] = 150;
-
options.c_cc[VMIN] = 0; /* Update the options and do it NOW */
-
if (tcsetattr (fd, TCSANOW, &options) != 0)
-
{
-
perror ("SetupSerial 3");
-
return (FALSE);
-
}
-
return (TRUE);
-
}
-
-
void
-
signal_handler_IO (int status)
-
{
-
printf ("received SIGIO signale.\n");
-
wait_flag = noflag;
-
}
-
-
int
-
main ()
-
{
-
printf ("This program updates last time at %s %s\n", __TIME__, __DATE__);
-
printf ("STDIO COM1\n");
-
int fd;
-
fd = open ("/dev/ttyUSB0", O_RDWR);
-
if (fd == -1)
-
{
-
perror ("serialport error\n");
-
}
-
else
-
{
-
printf ("open ");
-
printf ("%s", ttyname (fd));
-
printf (" succesfully\n");
-
}
-
-
set_speed (fd, 115200);
-
if (set_Parity (fd, 8, 1, 'N') == FALSE)
-
{
-
printf ("Set Parity Error\n");
-
exit (0);
-
}
-
-
char buf[255];
-
fd_set rd;
-
int nread = 0;
-
while(1)
-
{
-
FD_ZERO(&rd);
-
FD_SET(fd, &rd);
-
while(FD_ISSET(fd, &rd))
-
{
-
if(select(fd+1, &rd, NULL,NULL,NULL) < 0)
-
{
-
perror("select error\n");
-
}
-
else
-
{
-
while((nread = read(fd, buf, sizeof(buf))) > 0)
-
{
-
printf("nread = %d,%s\n",nread, buf);
-
printf("test\n");
-
memset(buf, 0 , sizeof(buf));
-
}
-
}
-
}
-
}
-
close (fd);
-
return 0;
-
}
-
4、示例解析
示例一:用来循环读取键盘输入。
常见的程序片段:
负值,select 错误;正值,某些文件可读、写或异常;0,等待超时,没有可读、写或异常的文件。
-
fs_set readset;
-
FD_ZERO(&readset);
-
FD_SET(fd,&readset);
-
select(fd+1,&readset,NULL,NULL,NULL);
-
if(FD_ISSET(fd,readset){……}
通过 select 返回值,做判断语句。负值,select 错误;正值,某些文件可读、写或异常;0,等待超时,没有可读、写或异常的文件。
这个之前有彻底的总结过的,从新开一篇文章说道说道,在此不做过多解释。
串口编程,除了通过 select 系统调用,在没有数据时阻塞进程,串口有数据需要读时唤醒进程。
还有两种方法,首先是最简单的循环读取程序,第二个是通过软中断方式,使用信号 signal 机制读取串口,这里需要注意的是硬件中断是设备驱动层级的,而读写串口是用户级行为,只能通过信号机制模拟中断,信号机制的发生和处理其实于硬件中断无异。
还有两种方法,首先是最简单的循环读取程序,第二个是通过软中断方式,使用信号 signal 机制读取串口,这里需要注意的是硬件中断是设备驱动层级的,而读写串口是用户级行为,只能通过信号机制模拟中断,信号机制的发生和处理其实于硬件中断无异。
-
//代码一:循环读取数据
-
#include<stdio.h>
-
#include<stdlib.h>
-
#include<unistd.h>
-
#include<sys/types.h>
-
#include<sys/stat.h>
-
#include<fcntl.h>
-
#include<termios.h>
-
#include<errno.h>
-
-
#define FALSE -1
-
#define TRUE 0
-
-
int speed_arr[] = { B38400, B19200, B9600, B4800, B2400, B1200, B300,B38400, B19200, B9600, B4800, B2400, B1200, B300, };
-
int name_arr[] = {38400, 19200, 9600, 4800, 2400, 1200, 300, 38400, 19200, 9600, 4800, 2400, 1200, 300, };
-
void set_speed(int fd, int speed){
-
int i;
-
int status;
-
struct termios Opt;
-
tcgetattr(fd, &Opt);
-
for ( i= 0; i < sizeof(speed_arr) / sizeof(int); i++) {
-
if (speed == name_arr[i]) {
-
tcflush(fd, TCIOFLUSH);
-
cfsetispeed(&Opt, speed_arr[i]);
-
cfsetospeed(&Opt, speed_arr[i]);
-
status = tcsetattr(fd, TCSANOW, &Opt);
-
if (status != 0) {
-
perror("tcsetattr fd1");
-
return;
-
}
-
tcflush(fd,TCIOFLUSH);
-
}
-
}
-
}
-
-
int set_Parity(int fd,int databits,int stopbits,int parity)
-
{
-
struct termios options;
-
if ( tcgetattr( fd,&options) != 0) {
-
perror("SetupSerial 1");
-
return(FALSE);
-
}
-
options.c_cflag &= ~CSIZE;
-
switch (databits)
-
{
-
case 7:
-
options.c_cflag |= CS7;
-
break;
-
case 8:
-
options.c_cflag |= CS8;
-
break;
-
default:
-
fprintf(stderr,"Unsupported data size\n"); return (FALSE);
-
}
-
switch (parity)
-
{
-
case 'n':
-
case 'N':
-
options.c_cflag &= ~PARENB; /* Clear parity enable */
-
options.c_iflag &= ~INPCK; /* Enable parity checking */
-
break;
-
case 'o':
-
case 'O':
-
options.c_cflag |= (PARODD | PARENB);
-
options.c_iflag |= INPCK; /* Disnable parity checking */
-
break;
-
case 'e':
-
case 'E':
-
options.c_cflag |= PARENB; /* Enable parity */
-
options.c_cflag &= ~PARODD;
-
options.c_iflag |= INPCK; /* Disnable parity checking */
-
break;
-
case 'S':
-
case 's': /*as no parity*/
-
options.c_cflag &= ~PARENB;
-
options.c_cflag &= ~CSTOPB;break;
-
default:
-
fprintf(stderr,"Unsupported parity\n");
-
return (FALSE);
-
}
-
-
switch (stopbits)
-
{
-
case 1:
-
options.c_cflag &= ~CSTOPB;
-
break;
-
case 2:
-
options.c_cflag |= CSTOPB;
-
break;
-
default:
-
fprintf(stderr,"Unsupported stop bits\n");
-
return (FALSE);
-
}
-
/* Set input parity option */
-
if (parity != 'n')
-
options.c_iflag |= INPCK;
-
tcflush(fd,TCIFLUSH);
-
options.c_cc[VTIME] = 150;
-
options.c_cc[VMIN] = 0; /* Update the options and do it NOW */
-
if (tcsetattr(fd,TCSANOW,&options) != 0)
-
{
-
perror("SetupSerial 3");
-
return (FALSE);
-
}
-
return (TRUE);
-
}
-
-
int main()
-
{
-
printf("This program updates last time at %s %s\n",__TIME__,__DATE__);
-
printf("STDIO COM1\n");
-
int fd;
-
fd = open("/dev/ttyS0",O_RDWR);
-
if(fd == -1)
-
{
-
perror("serialport error\n");
-
}
-
else
-
{
-
printf("open ");
-
printf("%s",ttyname(fd));
-
printf(" succesfully\n");
-
}
-
-
set_speed(fd,115200);
-
if (set_Parity(fd,8,1,'N') == FALSE) {
-
printf("Set Parity Error\n");
-
exit (0);
-
}
-
char buf[] = "fe55aa07bc010203040506073d";
-
write(fd,&buf,26);
-
char buff[512];
-
int nread;
-
while(1)
-
{
-
if((nread = read(fd, buff, 512))>0)
-
{
-
printf("\nLen: %d\n",nread);
-
buff[nread+1] = '\0';
-
printf("%s",buff);
-
}
-
}
-
close(fd);
-
return 0;
-
}
-
//代码二:通过signal机制读取数据
-
#include<stdio.h>
-
#include<stdlib.h>
-
#include<unistd.h>
-
#include<sys/types.h>
-
#include<sys/stat.h>
-
#include<sys/signal.h>
-
#include<fcntl.h>
-
#include<termios.h>
-
#include<errno.h>
-
-
#define FALSE -1
-
#define TRUE 0
-
#define flag 1
-
#define noflag 0
-
-
int wait_flag = noflag;
-
int STOP = 0;
-
int res;
-
-
int speed_arr[] =
-
{ B38400, B19200, B9600, B4800, B2400, B1200, B300, B38400, B19200, B9600,
-
B4800, B2400, B1200, B300, };
-
int name_arr[] =
-
{ 38400, 19200, 9600, 4800, 2400, 1200, 300, 38400, 19200, 9600, 4800, 2400,
-
1200, 300, };
-
void
-
set_speed (int fd, int speed)
-
{
-
int i;
-
int status;
-
struct termios Opt;
-
tcgetattr (fd, &Opt);
-
for (i = 0; i < sizeof (speed_arr) / sizeof (int); i++)
-
{
-
if (speed == name_arr[i])
-
{
-
tcflush (fd, TCIOFLUSH);
-
cfsetispeed (&Opt, speed_arr[i]);
-
cfsetospeed (&Opt, speed_arr[i]);
-
status = tcsetattr (fd, TCSANOW, &Opt);
-
if (status != 0)
-
{
-
perror ("tcsetattr fd1");
-
return;
-
}
-
tcflush (fd, TCIOFLUSH);
-
}
-
}
-
}
-
-
int
-
set_Parity (int fd, int databits, int stopbits, int parity)
-
{
-
struct termios options;
-
if (tcgetattr (fd, &options) != 0)
-
{
-
perror ("SetupSerial 1");
-
return (FALSE);
-
}
-
options.c_cflag &= ~CSIZE;
-
switch (databits)
-
{
-
case 7:
-
options.c_cflag |= CS7;
-
break;
-
case 8:
-
options.c_cflag |= CS8;
-
break;
-
default:
-
fprintf (stderr, "Unsupported data size\n");
-
return (FALSE);
-
}
-
switch (parity)
-
{
-
case 'n':
-
case 'N':
-
options.c_cflag &= ~PARENB; /* Clear parity enable */
-
options.c_iflag &= ~INPCK; /* Enable parity checking */
-
break;
-
case 'o':
-
case 'O':
-
options.c_cflag |= (PARODD | PARENB);
-
options.c_iflag |= INPCK; /* Disnable parity checking */
-
break;
-
case 'e':
-
case 'E':
-
options.c_cflag |= PARENB; /* Enable parity */
-
options.c_cflag &= ~PARODD;
-
options.c_iflag |= INPCK; /* Disnable parity checking */
-
break;
-
case 'S':
-
case 's': /*as no parity */
-
options.c_cflag &= ~PARENB;
-
options.c_cflag &= ~CSTOPB;
-
break;
-
default:
-
fprintf (stderr, "Unsupported parity\n");
-
return (FALSE);
-
}
-
-
switch (stopbits)
-
{
-
case 1:
-
options.c_cflag &= ~CSTOPB;
-
break;
-
case 2:
-
options.c_cflag |= CSTOPB;
-
break;
-
default:
-
fprintf (stderr, "Unsupported stop bits\n");
-
return (FALSE);
-
}
-
/* Set input parity option */
-
if (parity != 'n')
-
options.c_iflag |= INPCK;
-
tcflush (fd, TCIFLUSH);
-
options.c_cc[VTIME] = 150;
-
options.c_cc[VMIN] = 0; /* Update the options and do it NOW */
-
if (tcsetattr (fd, TCSANOW, &options) != 0)
-
{
-
perror ("SetupSerial 3");
-
return (FALSE);
-
}
-
return (TRUE);
-
}
-
-
void
-
signal_handler_IO (int status)
-
{
-
printf ("received SIGIO signale.\n");
-
wait_flag = noflag;
-
}
-
-
int
-
main ()
-
{
-
printf ("This program updates last time at %s %s\n", __TIME__, __DATE__);
-
printf ("STDIO COM1\n");
-
int fd;
-
struct sigaction saio;
-
fd = open ("/dev/ttyUSB0", O_RDWR);
-
if (fd == -1)
-
{
-
perror ("serialport error\n");
-
}
-
else
-
{
-
printf ("open ");
-
printf ("%s", ttyname (fd));
-
printf (" succesfully\n");
-
}
-
-
saio.sa_handler = signal_handler_IO;
-
sigemptyset (&saio.sa_mask);
-
saio.sa_flags = 0;
-
saio.sa_restorer = NULL;
-
sigaction (SIGIO, &saio, NULL);
-
-
//allow the process to receive SIGIO
-
fcntl (fd, F_SETOWN, getpid ());
-
//make the file descriptor asynchronous
-
fcntl (fd, F_SETFL, FASYNC);
-
-
set_speed (fd, 115200);
-
if (set_Parity (fd, 8, 1, 'N') == FALSE)
-
{
-
printf ("Set Parity Error\n");
-
exit (0);
-
}
-
-
char buf[255];
-
while (STOP == 0)
-
{
-
usleep (100000);
-
/* after receving SIGIO ,wait_flag = FALSE,input is availabe and can be read */
-
if (wait_flag == 0)
-
{
-
memset (buf, 0, sizeof(buf));
-
res = read (fd, buf, 255);
-
printf ("nread=%d,%s\n", res, buf);
-
// if (res ==1)
-
// STOP = 1; /*stop loop if only a CR was input */
-
wait_flag = flag; /*wait for new input */
-
}
-
}
-
-
-
close (fd);
-
return 0;
-
}
5、select 函数实现
下面我们分两个过程来分析select:
select 就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看 select 睡眠的详细过程。
select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动程序的 poll 函数。驱动程序提供的 poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个 bitmask 告诉 select 当前资源哪些可用。当 select 循环遍历完所有 fd_set 内指定的文件描述符对应的 poll 函数后,如果没有一个资源可用(即没有一个文件可供操作),则 select 让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout )继续往下执行。
(1) select的睡眠过程
支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的 BLOCK 或NONBLOCK 操作。当应用程序通过设备驱动访问该设备时(默认为 BLOCK 操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。select 就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看 select 睡眠的详细过程。
select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动程序的 poll 函数。驱动程序提供的 poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个 bitmask 告诉 select 当前资源哪些可用。当 select 循环遍历完所有 fd_set 内指定的文件描述符对应的 poll 函数后,如果没有一个资源可用(即没有一个文件可供操作),则 select 让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout )继续往下执行。
下面分析一下代码是如何实现的。
select 的调用 path 如下:sys_select -> core_sys_select -> do_select
查看 kernel/fs/select.c 其中最重要的函数是 do_select, 最主要的工作是在这里, 前面两个函数主要做一些准备工作。do_select 定义如下:
select 的调用 path 如下:sys_select -> core_sys_select -> do_select
查看 kernel/fs/select.c 其中最重要的函数是 do_select, 最主要的工作是在这里, 前面两个函数主要做一些准备工作。do_select 定义如下:
-
int do_select(int n, fd_set_bits *fds, s64 *timeout)
-
{
-
struct poll_wqueues table;
-
poll_table *wait;
-
int retval, i;
-
-
rcu_read_lock();
-
retval = max_select_fd(n, fds);
-
rcu_read_unlock();
-
-
if (retval < 0)
-
return retval;
-
n = retval;
-
-
poll_initwait(&table);
-
wait = &table.pt;
-
if (!*timeout)
-
wait = NULL;
-
retval = 0; //retval用于保存已经准备好的描述符数,初始为0
-
for (;;) {
-
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
-
long __timeout;
-
-
set_current_state(TASK_INTERRUPTIBLE); //将当前进程状态改为TASK_INTERRUPTIBLE
-
-
inp = fds->in; outp = fds->out; exp = fds->ex;
-
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
-
-
for (i = 0; i < n; ++rinp, ++routp, ++rexp) { //遍历每个描述符
-
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
-
unsigned long res_in = 0, res_out = 0, res_ex = 0;
-
const struct file_operations *f_op = NULL;
-
struct file *file = NULL;
-
-
in = *inp++; out = *outp++; ex = *exp++;
-
all_bits = in | out | ex;
-
if (all_bits == 0) {
-
i += __NFDBITS; // //如果这个字没有待查找的描述符, 跳过这个长字(32位)
-
continue;
-
}
-
-
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) { //遍历每个长字里的每个位
-
int fput_needed;
-
if (i >= n)
-
break;
-
if (!(bit & all_bits))
-
continue;
-
file = fget_light(i, &fput_needed);
-
if (file) {
-
f_op = file->f_op;
-
MARK(fs_select, "%d %lld",
-
i, (long long)*timeout);
-
mask = DEFAULT_POLLMASK;
-
if (f_op && f_op->poll)
-
/* 在这里循环调用所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数 */
-
mask = (*f_op->poll)(file, retval ? NULL : wait);
-
fput_light(file, fput_needed);
-
if ((mask & POLLIN_SET) && (in & bit)) {
-
res_in |= bit; //如果是这个描述符可读, 将这个位置位
-
retval++; //返回描述符个数加1
-
}
-
if ((mask & POLLOUT_SET) && (out & bit)) {
-
res_out |= bit;
-
retval++;
-
}
-
if ((mask & POLLEX_SET) && (ex & bit)) {
-
res_ex |= bit;
-
retval++;
-
}
-
}
-
cond_resched();
-
}
-
//返回结果
-
if (res_in)
-
*rinp = res_in;
-
if (res_out)
-
*routp = res_out;
-
if (res_ex)
-
*rexp = res_ex;
-
}
-
wait = NULL;
-
/* 到这里遍历结束。retval保存了检测到的可操作的文件描述符的个数。如果有文件可操作,则跳出for(;;)循环,直接返回。若没有文件可操作且timeout时间未到同时没有收到signal,则执行schedule_timeout睡眠。睡眠时间长短由__timeout决定,一直等到该进程被唤醒。
-
那该进程是如何被唤醒的?被谁唤醒的呢?
-
我们看下面的select唤醒过程*/
-
if (retval || !*timeout || signal_pending(current))
-
break;
-
if(table.error) {
-
retval = table.error;
-
break;
-
}
-
-
if (*timeout < 0) {
-
/* Wait indefinitely */
-
__timeout = MAX_SCHEDULE_TIMEOUT;
-
} else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
-
/* Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in a loop */
-
__timeout = MAX_SCHEDULE_TIMEOUT - 1;
-
*timeout -= __timeout;
-
} else {
-
__timeout = *timeout;
-
*timeout = 0;
-
}
-
__timeout = schedule_timeout(__timeout);
-
if (*timeout >= 0)
-
*timeout += __timeout;
-
}
-
__set_current_state(TASK_RUNNING);
-
-
poll_freewait(&table);
-
-
return retval;
-
}
(2)select的唤醒过程
前面介绍了 select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动程序的 poll 函数。驱动程序提供的poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个 bitmask 告诉 select 当前资源哪些可用。一个典型的驱动程序poll函数实现如下:
-
(摘自《Linux Device Drivers – ThirdEdition》Page 165)
-
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
-
{
-
struct scull_pipe *dev = filp->private_data;
-
unsigned int mask = 0;
-
/*
-
* The buffer is circular; it is considered full
-
* if "wp" is right behind "rp" and empty if the
-
* two are equal.
-
*/
-
down(&dev->sem);
-
poll_wait(filp, &dev->inq, wait);
-
poll_wait(filp, &dev->outq, wait);
-
if (dev->rp != dev->wp)
-
mask |= POLLIN | POLLRDNORM; /* readable */
-
if (spacefree(dev))
-
mask |= POLLOUT | POLLWRNORM; /* writable */
-
up(&dev->sem);
-
return mask;
-
}
将用户进程插入驱动的等待队列是通过poll_wait做的。Poll_wait定义如下:
-
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
-
{
-
if (p && wait_address)
-
p->qproc(filp, wait_address, p);
-
}
这里的p->qproc在do_select内poll_initwait(&table)被初始化为__pollwait,如下:
-
void poll_initwait(struct poll_wqueues *pwq)
-
{
-
init_poll_funcptr(&pwq->pt, __pollwait);
-
pwq->error = 0;
-
pwq->table = NULL;
-
pwq->inline_index = 0;
-
}
__pollwait定义如下:
-
/* Add a new entry */
-
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
-
poll_table *p)
-
{
-
struct poll_table_entry *entry = poll_get_entry(p);
-
if (!entry)
-
return;
-
get_file(filp);
-
entry->filp = filp;
-
entry->wait_address = wait_address;
-
init_waitqueue_entry(&entry->wait, current);
-
add_wait_queue(wait_address,&entry->wait);
-
}
通过 init_waitqueue_entry 初始化一个等待队列项,这个等待队列项关联的进程即当前调用 select 的进程。然后将这个等待队列项插入等待队列 wait_address。Wait_address 即在驱动 poll 函数内调用 poll_wait(filp, &dev->inq, wait);时传入的该驱动的 &dev->inq 或者 &dev->outq 等待队列。注: 关于等待队列的工作原理可以参考下面这篇文档:
参看:等待队列(二)
到这里我们明白了select如何当前进程插入所有所监测的fd_set关联的驱动内的等待队列,那进程究竟是何时让出CPU进入睡眠状态的呢?
进入睡眠状态是在do_select内调用schedule_timeout(__timeout)实现的。当select遍历完fd_set内的所有设备文件,发现没有文件可操作时(即retval=0),则调用schedule_timeout(__timeout)进入睡眠状态。
唤醒该进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。
举个例子,比如内核的8250 uart driver:
Uart是使用的Tty层维护的两个等待队列, 分别对应于读和写: (uart是tty设备的一种)
-
struct tty_struct {
-
……
-
wait_queue_head_t write_wait;
-
wait_queue_head_t read_wait;
-
……
-
}
当uart设备接收到数据,会调用tty_flip_buffer_push(tty);将收到的数据push到tty层的buffer。然后查看是否有进程睡眠的读等待队列上,如果有则唤醒该等待会列。
过程如下:
-
serial8250_interrupt -> serial8250_handle_port -> receive_chars -> tty_flip_buffer_push ->
-
flush_to_ldisc -> disc->receive_buf
-
在disc->receive_buf函数内:
-
if (waitqueue_active(&tty->read_wait)) //若有进程阻塞在read_wait上则唤醒
-
wake_up_interruptible(&tty->read_wait);
到这里明白了select进程被唤醒的过程。由于该进程是阻塞在所有监测的文件对应的设备等待队列上的,因此在timeout时间内,只要任意个设备变为可操作,都会立即唤醒该进程,从而继续往下执行。这就实现了select的当有一个文件描述符可操作时就立即唤醒执行的基本原理。
6、函数 pselect
-
#include <sys/select.h>
-
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
-
fd_set *exceptfds, const struct timespec *timeout,
-
const sigset_t *sigmask);
(1)pselect 与 select 不同之处
select 的超时值用 timeval 结构指定,但 pselect 使用 timespec 结构。timespec 结构以秒和纳秒表示超时值,而非秒和微妙。如果平台支持这样的时间精度,那么 timespec 就能提供精确的超时时间。
结构体 timespec 定义如下:
-
struct timespec {
-
long tv_sec; /* seconds */
-
long tv_nsec; /* nanoseconds */
-
};
pselect 的超时值被声明为 const,这保证了调用 pselect 不会改变此值。
pselect 可使用可选信号屏蔽字。若 sigmask 为 NULL,那么在与信号有关的方面,pselect 的运行状况和 select 相同。否则,sigmask 指向一信号屏蔽字,在调用 pselect 时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。
(2)示例说明
参看:pselect()
-
#include <time.h>
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <signal.h>
-
#include <unistd.h>
-
#include <sys/select.h>
-
#define BUFFSIZE 80
-
void sig_int(int signo);
-
void err_sys(const char *p_error);
-
void sig_alrm(int signo)
-
{
-
char s[] = "receive";
-
psignal(signo, s);
-
return;
-
}
-
int
-
main(int argc, char **argv)
-
{
-
int maxfdp1;
-
fd_set rset;
-
sigset_t sigmask;
-
ssize_t nread;
-
char buf[BUFFSIZE];
-
sigset_t sigset;
-
struct sigaction act;
-
// set SIGALRM signal handler
-
act.sa_handler = sig_alrm;
-
if (sigemptyset(&act.sa_mask) == -1)
-
err_sys("sigemptyset");
-
act.sa_flags = 0;
-
if (sigaction(SIGALRM, &act, NULL) == -1)
-
err_sys("sigaction");
-
// initialize signal set and addition SIGALRM into sigset
-
if (sigemptyset(&sigset) == -1)
-
err_sys("sigemptyet");
-
if (sigaddset(&sigset, SIGALRM) == -1)
-
err_sys("sigaddset");
-
alarm(1);
-
FD_ZERO(&rset);
-
FD_SET(STDIN_FILENO, &rset);
-
maxfdp1 = STDIN_FILENO + 1;
-
if (pselect(maxfdp1, &rset, NULL, NULL, NULL, &sigset) <= 0)
-
err_sys("pselect error");
-
if (FD_ISSET(STDIN_FILENO, &rset))
-
{
-
if ((nread = read(STDIN_FILENO, buf, BUFFSIZE)) == -1)
-
err_sys("read error");
-
if (write(STDOUT_FILENO, buf, nread) != nread)
-
err_sys("write error");
-
}
-
exit(0);
-
}
-
void
-
sig_int(int signo)
-
{
-
char s[] = "received";
-
psignal(signo, s);
-
return;
-
}
-
void
-
err_sys(const char *p_error)
-
{
-
perror(p_error);
-
exit(1);
-
}
-
输出结果:
-
d
-
receive: Alarm clock
-
d
上段代码如果没有 CTRL+C 送上一个 SIGINT 信号,将永远阻塞在与用户的交互上,ALARM 产生的 SIGALRM 信号永远打断不了 PSELECT, ALARM 信号被成功屏蔽。
三、函数 poll
poll 函数类似于 select,但是程序员结构有所不同。虽然 poll 函数时 system V 引入进来支持 STREAMS 子系统的,但是 poll 函数可用于任何类型的文件描述符。
-
#include <poll.h>
-
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1.
1、参数解析
与 select 不同,poll 不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,而是构造一个 pollfd 结构的数组,每个数组元素指定一个描述符 编号以及我们队该描述符感兴趣的条件。
-
struct pollfd {
-
int fd; /* file descriptor */
-
short events; /* requested events */
-
short revents; /* returned events */
-
};
fds 数组中的元素数由 nfds 指定。
应将每个数组元素的 events 成员设置为上图所示值的一个或几个,通过这些值告诉内核我们关心的是每个描述符的哪些事件。返回时,revnets 成员由内核设置,用于说明每个描述符发生了哪些事件。
注意,poll 没有更改 events 成员。这与 select 不同,select 修改其参数以指示哪个描述符已准备好了。
上图中的前 4 行测试的是可读性,接下来的 3 行测试的是可写性,最后 3 行测试的是异常条件。最后 3 行是由内核在返回时设置的。即使在 events 字段中没有指定这 3 个值,如果是相应条件发生,在 revents 中也会返回它们。
当一个描述符被挂断(POLLHUP)后,就不能再写该描述符,但是有可能仍然可以从该描述符读取到数据。
POLLIN | POLLPRI 等价于 select() 的读事件,POLLOUT |POLLWRBAND 等价于 select() 的写事件。POLLIN 等价于 POLLRDNORM |POLLRDBAND,而 POLLOUT 则等价于 POLLWRNORM。
查看 /usr/include/asm-generic/poll.h 可看到如下定义:
-
/* These are specified by iBCS2 */
-
#define POLLIN 0x0001
-
#define POLLPRI 0x0002
-
#define POLLOUT 0x0004
-
#define POLLERR 0x0008
-
#define POLLHUP 0x0010
-
#define POLLNVAL 0x0020
-
-
/* The rest seem to be more-or-less nonstandard. Check them! */
-
#define POLLRDNORM 0x0040
-
#define POLLRDBAND 0x0080
-
#ifndef POLLWRNORM
-
#define POLLWRNORM 0x0100
-
#endif
-
#ifndef POLLWRBAND
-
#define POLLWRBAND 0x0200
-
#endif
poll 的最后一个参数指定的是我们愿意等待多长时间。如同 select 一样,有 3 种不同的情形。
timeout == -1
永远等待。(某些系统在 <stropts.h>中定义了常量 INFTIM,其值通常是 -1)当所指定的描述符中的一个已准备好,或捕捉到一个信号时返回。如果捕捉到一个信号,则 poll 返回 -1,errno 设置为 EINTR。
timeout == 0
timeout == 0
不等待。测试所有描述符并立即返回。这是一种轮询系统的方法,可以找到多个描述符的状态而不阻塞 poll 函数。
上一篇: 025 UNIX再学习 -- 线程