【原創】Linux select/poll機制原理分析

前言

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

1. 概述

Linux系統在訪問設備的時候,存在如下幾種IO模型:node

  1. Blocking IO Model,阻塞IO模型
  2. Nonblocking I/O Model,非阻塞IO模型
  3. I/O Multiplexing Model,IO多路複用模型;
  4. Signal Driven I/O Model,信號驅動IO模型
  5. Asynchronous I/O Model,異步IO模型

今天咱們來分析下IO多路複用機制,在Linux中是經過select/poll/epoll機制來實現的。linux

先看一下阻塞IO模型與非阻塞IO模型的特色:服務器

  • 阻塞IO模型:在IO訪問的時候,若是條件沒有知足,會將當前任務切換出去,等到條件知足時再切換回來。
    • 缺點:阻塞IO操做,會讓處於同一個線程的執行邏輯都在阻塞期間沒法執行,這每每意味着須要建立單獨的線程來交互。
  • 非阻塞IO模型:在IO訪問的時候,若是條件沒有知足,直接返回,不會block該任務的後續操做。
    • 缺點:非阻塞IO須要用戶一直輪詢操做,輪詢可能會來帶CPU的佔用問題。

對單個設備IO操做時,問題並不嚴重,若是有多個設備呢?好比,在服務器中,監聽多個Client的收發處理,這時候IO多路複用就顯得尤其重要了,來張圖:數據結構

若是這個圖,讓你有點迷惑,那就像個男人同樣,man一下select/poll函數吧:異步

  • select:
    函數

  • poll
    oop

簡單來講,select/poll能監聽多個設備的文件描述符,只要有任何一個設備知足條件,select/poll就會返回,不然將進行睡眠等待。
看起來,select/poll像是一個管家了,統一負責來監聽處理了。測試

已經火燒眉毛來看看原理了,因爲底層的機制大致差很少,我將選擇select來作進一步分析。線程

2. 原理

2.1 select系統調用

select的系統調用開始:3d

  • select系統調用,最終的核心邏輯是在do_select函數中處理的,參考fs/select.c文件;
  • do_select函數中,有幾個關鍵的操做:
    1. 初始化poll_wqueues結構,包括幾個關鍵函數指針的初始化,用於驅動中進行回調處理;
    2. 循環遍歷監測的文件描述符,而且調用f_op->poll()函數,若是有監測條件知足,則會跳出循環;
    3. 在監測的文件描述符都不知足條件時,poll_schedule_timeout讓當前進程進行睡眠,超時喚醒,或者被所屬的等待隊列喚醒;
  • do_select函數的循環退出條件有三個:
    1. 檢測的文件描述符知足條件;
    2. 超時;
    3. 有信號要處理;
  • 在設備驅動程序中實現的poll()函數,會在do_select()中被調用,而驅動中的poll()函數,須要調用poll_wait()函數,poll_wait函數自己很簡單,就是去回調函數p->_qproc(),這個回調函數正是poll_initwait()函數中初始化的__pollwait()

因此,來看看__pollwait()函數嘍。

2.2 __pollwait

  • 驅動中的poll_wait函數回調__pollwait,這個函數完成的工做是向struct poll_wqueue結構中添加一條poll_table_entry
  • poll_table_entry中包含了等待隊列的相關數據結構;
  • 對等待隊列的相關數據結構進行初始化,包括設置等待隊列喚醒時的回調函數指針,設置成pollwake
  • 將任務添加到驅動程序中的等待隊列中,最終驅動能夠經過wake_up_interruptile等接口來喚醒處理;

這一頓操做,其實就是驅動向select維護的struct poll_wqueue中註冊,並將調用select的任務添加到驅動的等待隊列中,以便在合適的時機進行喚醒。因此,本質上來講,這是基於等待隊列的機制來實現的。

是否是還有點抽象,來看看數據結構的組織關係吧。

2.3 數據結構關係

  • 調用select系統調用的進程/線程,會維護一個struct poll_wqueues結構,其中兩個關鍵字段:
    1. pll_table:該結構體中的函數指針_qproc指向__pollwait函數;
    2. struct poll_table_entry[]:存放不一樣設備的poll_table_entry,這些條目的增長是在驅動調用poll_wait->__pollwait()時進行初始化並完成添加的;

2.4 驅動編寫啓示

若是驅動中要支持select的接口調用,那麼須要作哪些事情呢?
若是理解了上文中的內容,你會堅決果斷的大聲說出如下幾條:

  1. 定義一個等待隊列頭wait_queue_head_t,用於收留等待隊列任務;
  2. struct file_operations結構體中的poll函數須要實現,好比xxx_poll()
  3. xxx_poll()函數中,固然不要忘了poll_wait函數的調用了,此外,該函數的返回值mask須要注意是在條件知足時對應的值,好比EPOLLIN/EPOLL/EPOLLERR等,這個返回值是在do_select()函數中會去判斷處理的;
  4. 條件知足的時候,wake_up_interruptible喚醒任務,固然也可使用wake_up,區別是:wake_up_interruptible只能喚醒處於TASK_INTERRUPTIBLE狀態的任務,而wake_up能喚醒處於TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE狀態的任務;

2.5 select/poll的差別

  • selectpoll本質上基本相似,其中select是由BSD UNIX引入,pollSystemV引入;
  • selectpoll須要輪詢文件描述符集合,並在用戶態和內核態之間進行拷貝,在文件描述符不少的狀況下開銷會比較大,select默認支持的文件描述符數量是1024;
  • Linux提供了epoll機制,改進了selectpoll在效率與資源上的缺點,未深刻了解;

3. 示例代碼

3.1 內核驅動

示例代碼中的邏輯:

  1. 驅動維護一個count值,當count值大於0時,代表條件知足,poll返回正常的mask值;
  2. poll函數每執行一次,count值就減去一次;
  3. count的值能夠由用戶經過ioctl來進行設置;
#include <linux/init.h>
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/wait.h>
#include <linux/cdev.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <asm/ioctl.h>

#define POLL_DEV_NAME		"poll"

#define POLL_MAGIC		'P'
#define POLL_SET_COUNT      (_IOW(POLL_MAGIC, 0, unsigned int))

struct poll_dev {
	struct cdev cdev;
	struct class *class;
	struct device *device;

	wait_queue_head_t wq_head;

	struct mutex poll_mutex;
	unsigned int count;

	dev_t devno;
};

struct poll_dev *g_poll_dev = NULL;

static int poll_open(struct inode *inode, struct file *filp)
{
	filp->private_data = g_poll_dev;

	return 0;
}

static int poll_close(struct inode *inode, struct file *filp)
{
	return 0;
}

static unsigned int poll_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct poll_dev *dev = filp->private_data;

	mutex_lock(&dev->poll_mutex);

	poll_wait(filp, &dev->wq_head, wait);

	if (dev->count > 0) {
		mask |= POLLIN | POLLRDNORM;

		/* decrease each time */
		dev->count--;
	}
	mutex_unlock(&dev->poll_mutex);

	return mask;
}

static long poll_ioctl(struct file *filp, unsigned int cmd,
		unsigned long arg)
{
	struct poll_dev *dev = filp->private_data;
	unsigned int cnt;

	switch (cmd) {
		case POLL_SET_COUNT:
			mutex_lock(&dev->poll_mutex);
			if (copy_from_user(&cnt, (void __user *)arg, _IOC_SIZE(cmd))) {
				pr_err("copy_from_user fail:%d\n", __LINE__);
				return -EFAULT;
			}

			if (dev->count == 0) {
				wake_up_interruptible(&dev->wq_head);
			}

			/* update count */
			dev->count += cnt;

			mutex_unlock(&dev->poll_mutex);
			break;
		default:
			return -EINVAL;
	}

	return 0;
}

static struct file_operations poll_fops = {
	.owner = THIS_MODULE,
	.open = poll_open,
	.release = poll_close,
	.poll = poll_poll,
	.unlocked_ioctl = poll_ioctl,
	.compat_ioctl = poll_ioctl,
};

static int __init poll_init(void)
{
	int ret;

	if (g_poll_dev == NULL) {
		g_poll_dev = (struct poll_dev *)kzalloc(sizeof(struct poll_dev), GFP_KERNEL);
		if (g_poll_dev == NULL) {
			pr_err("struct poll_dev allocate fail\n");
			return -1;
		}
	}

	/* allocate device number */
	ret = alloc_chrdev_region(&g_poll_dev->devno, 0, 1, POLL_DEV_NAME);
	if (ret < 0) {
		pr_err("alloc_chrdev_region fail:%d\n", ret);
		goto alloc_chrdev_err;
	}

	/* set char-device */
	cdev_init(&g_poll_dev->cdev, &poll_fops);
	g_poll_dev->cdev.owner = THIS_MODULE;
	ret = cdev_add(&g_poll_dev->cdev, g_poll_dev->devno, 1);
	if (ret < 0) {
		pr_err("cdev_add fail:%d\n", ret);
		goto cdev_add_err;
	}

	/* create device */
	g_poll_dev->class = class_create(THIS_MODULE, POLL_DEV_NAME);
	if (IS_ERR(g_poll_dev->class)) {
		pr_err("class_create fail\n");
		goto class_create_err;
	}
	g_poll_dev->device = device_create(g_poll_dev->class, NULL,
			g_poll_dev->devno, NULL, POLL_DEV_NAME);
	if (IS_ERR(g_poll_dev->device)) {
		pr_err("device_create fail\n");
		goto device_create_err;
	}

	mutex_init(&g_poll_dev->poll_mutex);
	init_waitqueue_head(&g_poll_dev->wq_head);

	return 0;

device_create_err:
	class_destroy(g_poll_dev->class);
class_create_err:
	cdev_del(&g_poll_dev->cdev);
cdev_add_err:
	unregister_chrdev_region(g_poll_dev->devno, 1);
alloc_chrdev_err:
	kfree(g_poll_dev);
	g_poll_dev = NULL;
	return -1;
}

static void __exit poll_exit(void)
{
	cdev_del(&g_poll_dev->cdev);
	device_destroy(g_poll_dev->class, g_poll_dev->devno);
	unregister_chrdev_region(g_poll_dev->devno, 1);
	class_destroy(g_poll_dev->class);

	kfree(g_poll_dev);
	g_poll_dev = NULL;
}

module_init(poll_init);
module_exit(poll_exit);

MODULE_DESCRIPTION("select/poll test");
MODULE_AUTHOR("LoyenWang");
MODULE_LICENSE("GPL");

3.2 測試代碼

測試代碼邏輯:

  1. 建立一個設值線程,用於每隔2秒來設置一次count值;
  2. 主線程調用select函數監聽,當設值線程設置了count值後,select便會返回;
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>

static void *set_count_thread(void *arg)
{
	int fd = *(int *)arg;
	unsigned int count_value = 1;
	int loop_cnt = 20;
	int ret;

	while (loop_cnt--) {
		ret = ioctl(fd, NOTIFY_SET_COUNT, &count_value);
		if (ret < 0) {
			printf("ioctl set count value fail:%s\n", strerror(errno));
			return NULL;
		}

		sleep(1);
	}

	return NULL;
}

int main(void)
{
	int fd;
	int ret;
	pthread_t setcnt_tid;
	int loop_cnt = 20;

	/* for select use */
	fd_set rfds;
	struct timeval tv;

	fd = open("/dev/poll", O_RDWR);
	if (fd < 0) {
		printf("/dev/poll open failed: %s\n", strerror(errno));
		return -1;
	}

	/* wait up to five seconds */
	tv.tv_sec = 5;
	tv.tv_usec = 0;

	ret = pthread_create(&setcnt_tid, NULL,
			set_count_thread, &fd);
	if (ret < 0) {
		printf("set_count_thread create fail: %d\n", ret);
		return -1;
	}

	while (loop_cnt--) {
		FD_ZERO(&rfds);
		FD_SET(fd, &rfds);

		ret = select(fd + 1, &rfds, NULL, NULL, &tv);
		//ret = select(fd + 1, &rfds, NULL, NULL, NULL);
		if (ret == -1) {
			perror("select()");
			break;
		}
		else if (ret)
			printf("Data is available now.\n");
		else {
			printf("No data within five seconds.\n");
		}
	}

	ret = pthread_join(setcnt_tid, NULL);
	if (ret < 0) {
		printf("set_count_thread join fail.\n");
		return -1;
	}

	close(fd);

	return 0;
}

相關文章
相關標籤/搜索