Skip to content

LV035-多路复用IO-02-poll

本文主要是网络编程——多路复用IO中的poll的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、poll相关函数

1. poll()函数

1.1 函数说明

linux下可以使用man 2 poll命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <poll.h>

/* 函数声明 */
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数说明】该函数用于实现I/O多路复用,它与select类似,但是去除了select中文件描述符数量(select一般最大是1024)的限制,并且也取消了select用三个位图描述,而用整体的pollfd结构体指针实现。在poll()函数中,我们需要构造一个struct pollfd类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。

函数参数

  • fdsstruct pollfd类型的结构体指针变量,我们一般会用它指向一个struct pollfd类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件。struct pollfd 结构体的详细说明如下:

我们在使用man手册的时候,下边会有该结构体的原型:

c
struct pollfd
{
	int   fd;         /* file descriptor */
	short events;     /* requested events */
	short revents;    /* returned events */
};
// `fd`:`int`类型,是一个文件描述符,用于表示我们关心的文件描述符。
// `events`:`short`类型,位掩码,我们初始化`events`成员来指定需要为文件描述符`fd`做检查的事件。
// `revents`:`short`类型,位掩码,当`poll()`函数返回时,`revents`变量由`poll()`函数内部进行设置,用于说明文件描述符`fd`发生了哪些事件,这里需要注意,`poll()`没有更改`events`变量,我们可以对`revents`进行检查,判断文件描述符`fd`发生了什么事件。

我们应将每个数组元素的events成员设置成下表中的一个或几个标志,多个标志通过位或运算符( | )组合起来,通过这些值告诉内核我们关心的是该文件描述符的哪些事件。同样,返回时,revents变量由内核设置为下表所示的一个或几个标志。

分组取值是否可作为events的值是否可作为revents的值说明
第一组POLLIN有数据可以读取
POLLRDNORM有数据可以读取,等同于 POLLIN
POLLRDBAND可以读取优先级数据(Linux上通常不使用)
POLLPRI可读取高优先级数据
POLLRDHUP对端套接字关闭
第二组POLLOUT可写入数据
POLLWRNORM 可写入数据,等同于 POLLOUT
POLLWRBAND 优先级数据可写入
第三组POLLERR有错误发生
POLLHUP发生挂起
POLLNVAL文件描述符未打开
其中,第一组标志与数据可读相关;第二组标志与可写数据相关;第三组标志是设定在`revents`变量中用来返回有关文件描述符的附加信息,如果在`events`变量中指定了这三个标志,则会被忽略,相当于没有设置。

注意】如果我们对某个文件描述符上的事件不感兴趣,则可将events变量设置为0;另外,将fd变量设置为文件描述符的负值(取文件描述符fd的相反数-fd),将导致对应的events变量被poll()忽略,并且revents变量将总是返回0,这两种方法都可用来关闭对某个文件描述符的检查。

  • nfdsnfds_t类型,指定了fds数组中的元素个数(我的理解就是我们定义的数组中实际可以使用的元素的个数),数据类型nfds_t实际为无符号整形。
  • timeoutint类型,用于决定poll()函数的阻塞行为,单位为ms
timeout = -1 poll() 会一直阻塞(与 select() 函数的 timeout 等于 NULL 相同),直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。
timeout = 0 poll() 不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。
timeout >0表示设置 poll() 函数阻塞时间的上限值,意味着 poll() 函数最多阻塞 timeout 毫秒,直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止。
【**返回值**】`int`类型,返回值与`select()`类似,有以下几种情况:
  • 返回-1:表示有错误发生,并且会设置errno表示错误类型。
  • 返回0:表示该调用在任意一个文件描述符成为就绪态之前就超时了。
  • 返回一个正整数:表示有一个或多个文件描述符处于就绪态,这个时候的返回值具体表示fds数组中返回的revents变量不为0struct pollfd对象的数量。

使用格式】后边直接看实例就好。

注意事项

(1)每当调用poll()函数之后,系统不会清空这个struct pollfd类型数组。

1.2 使用实例

暂无

2. ppoll()函数

2.1 函数说明

linux下可以使用man 2 ppoll命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <poll.h>

/* 函数声明 */
int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *tmo_p, const sigset_t *sigmask);

函数说明】该函数用于实现I/O多路复用,该函数是一个 防止信号干扰的增强型 poll()函数。

函数参数

  • fdsstruct pollfd类型的结构体指针变量,该参数与poll()一样,我们一般会用它指向一个struct pollfd类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件。我们在使用man手册的时候,下边会有该结构体的原型:
c
struct pollfd
{
	int   fd;         /* file descriptor */
	short events;     /* requested events */
	short revents;    /* returned events */
};
// `fd`:`int`类型,是一个文件描述符,用于表示我们关心的文件描述符。
// `events`:`short`类型,位掩码,我们初始化`events`成员来指定需要为文件描述符`fd`做检查的事件。
// `revents`:`short`类型,位掩码,当`poll()`函数返回时,`revents`变量由`poll()`函数内部进行设置,用于说明文件描述符`fd`发生了哪些事件,这里需要注意,`poll()`没有更改`events`变量,我们可以对`revents`进行检查,判断文件描述符`fd`发生了什么事件。
  • nfdsnfds_t类型,该参数与poll()一样,指定了fds数组中的元素个数(我的理解就是我们定义的数组中实际可以使用的元素的个数),数据类型nfds_t实际为无符号整形。
  • tmo_pstruct timespec类型,用于决定ppoll()函数的阻塞行为,如果设置为NULL,那么ppoll()函数将会一直阻塞。在使用man手册的时候,会有 struct timespec结构体的说明,该结构体的成员如下:
c
struct timespec
{
	long    tv_sec;         /* seconds */
	long    tv_nsec;        /* nanoseconds */
};
  • sigmasksigset_t类型指针变量,表示信号掩码,指定一个需要屏蔽的信号集。

返回值int类型,返回值与poll()一样,有以下几种情况:

  • 返回-1:表示有错误发生,并且会设置errno表示错误类型。
  • 返回0:表示该调用在任意一个文件描述符成为就绪态之前就超时了。
  • 返回一个正整数:表示有一个或多个文件描述符处于就绪态,这个时候的返回值具体表示fds数组中返回的revents变量不为0struct pollfd对象的数量。

使用格式none

注意事项none

2.2 使用实例

暂无

二、poll()使用实例

这里我们就不使用服务器和客户端的例子了(主要是自己觉得客户端关闭的时候,服务器端处理较为麻烦,后边暂时还有更重要的东西要学习,所以这里先写一个其他的实例,后边用到了再补充吧),我们来尝试一种新的方式:读取鼠标和键盘,键盘的话是标准输入,文件描述符为0,那鼠标呢?它其实也是一种输入设备,它对应的设备文件在/dev/intput目录下,我们输入以下命令,查看我当前使用的Ubuntu下边的输入设备:

shell
ls /dev/input

然后我们会看到如下信息输出:

image-20220703171421815

这里有很多个文件,我们怎么知道哪个是鼠标呢?通常情况下是mouseXX表示序号012),但也不一定,也有可能是eventX,如何确定到底是哪个设备文件,可以通过对设备文件进行读取来判断,例如od命令:

c
sudo od -x /dev/input/event2

注意,这里需要添加sudo,在Ubuntu系统下,普通用户是无法对设备文件进行读取或写入操作。当执行命令之后,移动鼠标或按下鼠标、松开鼠标都会在终端打印出相应的数据的话,这个便是代表鼠标的设备文件啦:

image-20220703171650804

接下来我们就开始写我们的poll()函数实例,使用poll()来检测键盘和鼠标的数据:

c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>

#define MOUSE "/dev/input/event2"

int main(void)
{
	char buf[100];
	int fd = -1;
	int ret = 0;
	int flag;
	struct pollfd fds[2]; 
	/* 打开鼠标设备文件 */
	fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
	if (fd < 0)
	{
		perror("open error");
		exit(-1);
	}
	/* 将键盘设置为非阻塞方式 */
	flag = fcntl(0, F_GETFL); /* 先获取原来的flag */
	flag |= O_NONBLOCK;       /* 将O_NONBLOCK标准添加到flag */
	fcntl(0, F_SETFL, flag);  /* 重新设置flag */
	/* 同时读取键盘和鼠标 */
	fds[0].fd = 0;
	fds[0].events = POLLIN; /* 只关心数据可读 */
	fds[0].revents = 0;
	fds[1].fd = fd;
	fds[1].events = POLLIN; /* 只关心数据可读 */
	fds[1].revents = 0;
	while(1)
	{
		ret = poll(fds, 2, -1);
		if (ret < 0)
		{
			perror("poll error");
			break;
		}
		else if (ret == 0)
		{
			fprintf(stderr, "poll timeout!\n");
			continue;
		}
		/* 检查键盘是否为就绪态 */
		if(fds[0].revents & POLLIN)
		{
			ret = read(0, buf, sizeof(buf));
			if (ret > 0)
				printf("keyboard: read<%d>Bytes data!\n", ret);
		} 
		/* 检查鼠标是否为就绪态 */
		if(fds[1].revents & POLLIN)
		{
			ret = read(fd, buf, sizeof(buf));
			if (ret > 0)
				printf("mouse: read<%d>Bytes data!\n", ret);
		}
	}
	close(fd);
	return 0;
}

然后我们执行以下命令,编译和运行程序:

shell
gcc test.c -Wall # 生成可执行文件 a.out 
sudo ./a.out     # 执行可执行程序

由于这里用到了鼠标的设备文件,所以这里需要加上sudo然后,终端会有以下信息显示:

image-20220703172628646