设为首页 加入收藏

TOP

调试linux内核(2): poll系统调用的实现(一)
2023-08-26 21:10:19 】 浏览:66
Tags:调试 linux 内核 poll

linux内核为用户态进程提供了一组IO相关的系统调用: select/poll/epoll, 这三个系统调用功能类似, 在使用方法和性能等方面存在一些差异. 使用它们, 用户态的进程可以"监控"自己感兴趣的文件描述符, 当这些文件描述符的状态发生改变时, 比如可读或者可写了, 内核会通知进程去处理, 这里的文件描述符可以是socket, 设备文件, 管道等. 使用这组系统调用, 用户态可以实现事件循环机制, 比如redis源码中就基于此实现了自己内部使用的事件循环, 同样还有很多其他专门提供事件循环机制的开源库. 这里通过一个驱动模块实现的poll接口, 去分析内核中poll系统调用的实现原理. 主要讨论了以下3个问题:

  1. 用户态进程如何使用poll系统调用?
  2. 内核如何处理poll系统调用?
  3. 怎样调试从进程发起poll调用到返回的过程?

问题1

用户态进程如何使用poll系统调用?

简单来说, 使用poll的时候, 进程需要告诉内核自己关心哪些文件描述符, 关心它们的什么事件, 这些都是通过参数传递给poll系统调用的. 下面是手册中对poll的详细说明:

POLL(2)                                                                                                                       Linux Programmer's Manual                                                                                                                       POLL(2)

NAME
       poll, ppoll - wait for some event on a file descriptor

SYNOPSIS
       #include <poll.h>

       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

       #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <signal.h>
       #include <poll.h>

       int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *tmo_p, const sigset_t *sigmask);

DESCRIPTION
       poll() performs a similar task to select(2): it waits for one of a set of file descriptors to become ready to perform I/O.  The Linux-specific epoll(7) API performs a similar task, but offers features beyond those found in poll().

       The set of file descriptors to be monitored is specified in the fds argument, which is an array of structures of the following form:

           struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

       The caller should specify the number of items in the fds array in nfds.

poll接受三个参数, 其中pollfd的数组用来告诉内核, 进程关心哪些文件描述符, 结构体的fd字段是文件描述符的值, events是关心的事件, 比如希望fd可读时收到内核通知, 就可以设为POLL_IN, 这个events字段支持位或, 也就是关心的多种事件可以用或运算一起计算出events的最终数值, revents字段表示poll系统调用返回之后, 在该fd上发生的事件. poll的第二和第三个参数分别表示数组的大小和超时时间, 其中timeout以毫秒为单位, 如果timeout==0, poll会立即返回, 如果timeout < 0, poll会一直等待, 直到fds中期待的事件发生, 或者进程收到信号, 或者其他原因进程退出了. 当fds中的事件没有发生或者超时时间没到时, 进程就会处于睡眠状态. poll的返回值反映了三种可能的结果, 1) 出错, 2) 超时, 3) 发生期待事件的fd的数量. 其他的信息可以自行阅读manual.

以下代码会用来发起poll调用, 然后调试poll的实现:

/*ignore include headers*/
int main(int argc, char *argv[])
{
	int dev_fd = open("/dev/cdev03", O_RDWR);
	if (dev_fd < 0) {
		perror("Can not open device file");
		return -1;
	}

	struct pollfd pollfd = {
		.fd = dev_fd,
		.events = POLL_IN,
		.revents = 0,
	};

	char buf[1024];
	int max_poll_calls = 3;
	while (max_poll_calls) {
		int ret = poll(&pollfd, 1, -1);
		if (ret == 1) {
			memset(buf, 0, 1024);
			read(dev_fd, buf, 1024);
			printf("poll_reader recv data: %s\n", buf);
		}
		max_poll_calls--;
	}

	close(dev_fd);
	return 0;
}

代码中poll设备文件"/dev/cdev03"的状态变化, 在poll三次之后退出.

问题2

内核如何处理poll系统调用?

因为进程传递给内核的可能是多个文件描述符, 所以在poll的实现中也需要遍历这些fd并检查它们的状态, 实际poll的实现涉及到比较多的数据结构, 这里先简单概括一下进入到poll系统调用之后内核的处理逻辑:

# ATTENION: we are in poll syscall now

0) 计算超时状态初始值;

while True:
	for fd in fds:
		1) 获取当前fd的状态;
		2) 记录fd的状态;
		3) 对符合条件的fd计数;

	if 存在符合条件的fd 或者 超时时间到:
		break

	4) 调用schedule相关API, 让出CPU, 当前进程开始带有超时的睡眠;

	5) 更新超时状态;
	# 如果进程被唤醒, schedule调用就会返回, 进程将在内核态, 继续这个循环

以上就是poll实现中的核心逻辑, 当然, 实际情况还是会稍微复杂亿点的, 以上描述中省略了进程被信号唤醒等处理逻辑. 后面会用一个字符设备驱动, 跟踪这个实现过程, 下面是设备驱动和poll系统调用在交互过程中各自的职责划分:

  • 内核怎么获取fd的状态?
    对于字符设备驱动, 它要实现file_operations中的poll接口, 内核在步骤1)会调用, 得到设备的状态
  • 设备怎么通知进程设备的状态发生了变化?
    在设备驱动实现的poll接口被调用时, 会使用poll_wait传递给调用者一个等待队列, 调用驱动接口的上层代码在这个队列中插入元素, 通过这个元素, 可以间接找到睡眠的进程, 当有数据写入设备时, 驱动模块的write接口被调用,
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇系统内存管理:虚拟内存、内存分.. 下一篇SELinux 入门 pt.2

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目