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

来源:https://www.cnblogs.com/LoyenWang/archive/2020/04/02/12622904.html
-Advertisement-
Play Games

前言 By 魯迅 By 高爾基 1. 概述 Linux系統在訪問設備的時候,存在以下幾種IO模型: 1. ; 2. ; 3. ; 4. ; 5. ; 今天我們來分析下IO多路復用機制,在Linux中是通過 機制來實現的。 先看一下阻塞IO模型與非阻塞IO模型的特點: 阻塞IO模型:在IO訪問的時候, ...


前言

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

1. 概述

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

  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機制來實現的。

先看一下阻塞IO模型與非阻塞IO模型的特點:

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

對單個設備IO操作時,問題並不嚴重,如果有多個設備呢?比如,在伺服器中,監聽多個Client的收發處理,這時候IO多路復用就顯得尤為重要了,來張圖:

如果這個圖,讓你有點迷惑,那就像個男人一樣,man一下select/poll函數吧:

  • select:

  • poll

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

已經迫不及待來看看原理了,由於底層的機制大體差不多,我將選擇select來做進一步分析。

2. 原理

2.1 select系統調用

select的系統調用開始:

  • 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;
}


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 下麵的靜態代碼中: 現在想把箭頭所指的值,改為動態。 根據不同條件,它將有可能是1,或是3或是2或是5等。 ...
  • 四、C#表達式與運算符 4.1.表達式 操作數+運算符 4.2.數學運算符 var++ 先用後加 ++var 先加後用 4.3.賦值運算符 略 4.4.關係運算符 結果只會是bool類型 1)對象的不同 數值類型比較兩個數的大小 字元類比較Unicode編碼大小,'A'=65 'a'=97 '0'= ...
  • 三、C#數據類型 3.1.變數 聲明->賦值->使用 作用域:變數作用域為包含它的大括弧內 3.2.常量 1)const 數據類型 常量名稱 = 常量值 聲明常量時一定要賦值 2)@作用 輸出轉義字元 @"Hello World\n" 讓字元串換行 關鍵字用作標識符 @namespace @clas ...
  • 傳遞數據至部分視圖: 在ps.cshtml中get到上面高亮的參數: ...
  • VS2013如何轉成VS2010且不會出現此項目與Visual Studio的當前版本不相容的報錯 解決方法: 1.用記事本打開解決方案文件“解決方案名.sln”,然後修改最上面兩行為如下代碼:Microsoft Visual Studio Solution File, Format Version ...
  • [toc] 1.背景 接上篇文章 "深入淺出C 結構體——封裝乙太網心跳包的結構為例" ,使用結構體性能不佳,而且也說明瞭原因。本篇文章詳細描述了以類來封裝網路心跳包的優缺點,結果大大提升瞭解析性能。 2.用類來封裝乙太網心跳包的優缺點 2.1.優點 + 可以在類里直接new byte[],即直接實 ...
  • 前言: gRPC預設是ProtoFirst的,即先寫 proto文件,再生成代碼,需要人工維護proto,生成的代碼也不友好,所以出現了gRPC CodeFirst,下麵來說說我們是怎麼實現gRPC CodeFirst 目錄: 實現和WCF一樣的CodeFirst (1). 實現gRPC CodeF ...
  • 本文將介紹如何在.NET Core3環境下使用MVVM框架Prism的使用區域管理器對於View的管理 一.區域管理器 我們在之前的Prism系列構建了一個標準式Prism項目,這篇文章將會講解之前項目中用到的利用區域管理器更好的對我們的View進行管理,同樣的我們來看看官方給出的模型圖: 現在我們 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...