Linux的異步通知

前言

本文主要是我學習Linux的異步通知機制所做的筆記,介紹了異步通知的一些應用層和驅動層的接口,內容會不斷的完善。

所涉及到的源碼的kernel版本是linux-kernel-5.3.1

1 概覽異步通知

當我們訪問文件的時候,比如讀/寫,如果當前不能讀或不能寫,那麼我們可以選擇阻塞訪問,直到有數據可讀或有空間可寫。而異步通知是進程先打開設備文件,再通過fcntl函數,設置F_SETOWNFASYNC標誌,從而告知相應的設備:想要訪問你的進程具體是哪個,且想要以異步通知的方式訪問你。起到的效果是可讀/可寫的時候,驅動層會給進程發信號,進程收到信號之後執行相應的處理函數,在處理函數中進行讀/寫操作。進程不需要管什麼時候文件是可訪問的,就像你點了外賣以後,不需要死等,你只要接着幹自己的事情就行,外賣小哥到了會給你打電話(發信號)的。

我們以一幅圖來概括一下Linux的異步通知,更多的細節後面會分析:
在這裏插入圖片描述

上文所說的進程文件之間的關係可以用下圖描述:
在這裏插入圖片描述

2 應用層使用異步通知

應用程序使用異步通知的一個例子如下(僅爲說明問題,因此沒有對各個系統調用進行錯誤處理):

...

// 記錄要訪問的文件的描述符
static int fd;

// 信號處理函數
void signal_handler(int signum, siginfo_t *siginfo, void *act)
{
	...
	if (signum == SIGIO)
	{
		if (siginfo->si_band & POLLIN)
		{
			// 處理數據可讀的情況
			...
		}
		
		if (siginfo->si_band & POLLOUT)
		{
			// 處理數據可寫的情況
			...
		}
	}
}


int main()
{
	int ret, flag;
	struct sigaction act, oldact;
	
	// 填充act結構
	sigemptyset(&act.sa_mask);
	sigaddset(&act.sa_mask, SIGIO);
	act.sa_flags = SA_SIGINFO;
	act.sa_sigaction = signal_handler;
	
	// 綁定信號SIGIO及其處理函數
	sigaction(SIGIO, &act, &oldact);	
	
	// 打開要訪問的文件
	fd = open("/dev/my_dev", O_RDWR);

	// 設置文件結構的f_owner字段,進而告訴驅動信號要發給哪個進程
	fcntl(fd, F_SETOWN, getpid());
	
	// 這條代碼實際上是設置struct file的f_owner成員的signum成員
	// signum被設置後會帶來一些影響,比如send_sigio_to_task不再默認發送SIGIO信號,而是發送signum指定的信號
	// 再比如kernel_siginfo_t結構體被填充(這個結構應該是傳給應用層的siginfo_t)
	// 下文中驅動的部分還會講到它(吐槽一下man手冊,F_SETSIG介紹的不是很清楚呀,也可能是我太菜了-_-!!)
	fcntl(fd, F_SETSIG, SIGIO);
	
	// 設置FASYNC標誌,使能異步通知
	flag = fcntl(fd, F_GETFL);
	fcntl(fd, F_SETFL, flag | FASYNC);
	
	// 到這裏進程就可以做其他事情,不用管文件訪問了,可讀/可寫時,驅動給進程發信號,進程自動執行signal_handler訪問文件
	....
	
	return 0;
}

3 驅動層支持異步通知

3.1 響應應用層設置FASYNC——xxx_fasync

當我們在應用層調用fcntl(fd, F_SETFL, flag | FASYNC)時,最終會調用到驅動層的struct file_operationsfasync成員,因此我們需要在驅動層實現xxx_fasync,這個函數一般這麼寫:

static int xxx_fasync(int fd, struct file *file, int on)
{
	...
	return fasync_helper(fd, file, on, &device->fasync);
}

其中device一般是我們定義的設備結構體,在這個結構體中,我們會定義一個成員struct fasync_struct *fasyncstruct fasync_struct的定義如下:

struct fasync_struct {
	rwlock_t		fa_lock;          // 讀寫鎖:用於同步併發的訪問
	int			magic;
	int			fa_fd;                // 記錄文件描述符
	struct fasync_struct	*fa_next; // 構成單向鏈表
	struct file		*fa_file;         // 指向文件結構,文件結構的f_owner字段記錄了調用fcntl(fd, F_SETOWN, getpid())的進程的pid
	struct rcu_head		fa_rcu;       // 用於RCU機制
};

可以看到xxx_fasync調用了fasync_helper,概括的說,這個函數會申請一個struct fasync_struct類型的變量,這個變量裏面記錄了信號要發給哪個進程。實際上這個變量會記錄一個文件結構體指針,指向fd所表示的文件結構體,而文件結構體中的f_owner字段才記錄了進程的pid。當然,我們需要先調用fcntl(fd, F_SETOWN, getpid()),這個函數會設置f_owner字段。

進一步的,我們可以簡單的跟蹤一下這個函數,它做的事情其實很簡單:

3.1.1 fasync_helper

int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
	// 使用fcntl設置FASYNC的時候on爲1,清除FASYNC的時候on爲0
	if (!on)
		// 刪除struct fasync_struct結構(叢鏈表移除並釋放)
		return fasync_remove_entry(filp, fapp);

	// 添加struct fasync_struct結構(申請空間並插入鏈表)
	return fasync_add_entry(fd, filp, fapp);
}

3.1.2 fasync_add_entry

static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
{
	struct fasync_struct *new;
	// 申請struct fasync_struct類型的空間
	new = fasync_alloc();
	if (!new)
		return -ENOMEM;

	// 如果當前struct fasync_struct鏈表中已經有指向filp所指向的文件結構的項
	// 那麼fasync_insert_entry會更新該項的fa_fd,並返回該項的地址(非零值)
	// 否則將新的項(new)插入鏈表
	if (fasync_insert_entry(fd, filp, fapp, new)) {
		// 如果(new)沒有被插入鏈表那麼就釋放它
		fasync_free(new);
		return 0;
	}

	return 1;
}

3.1.3 fasync_insert_entry

struct fasync_struct *fasync_insert_entry(int fd, struct file *filp, struct fasync_struct **fapp, struct fasync_struct *new)
{
	struct fasync_struct *fa, **fp;
	// 加鎖:保證對filp指向的文件結構的訪問互斥
	spin_lock(&filp->f_lock);
	// 加鎖:保證對設備的struct fasync_struct鏈表(異步通知鏈表)的訪問互斥
	spin_lock(&fasync_lock);

	// 遍歷設備的異步通知鏈表
	for (fp = fapp; (fa = *fp) != NULL; fp = &fa->fa_next) {
		if (fa->fa_file != filp)
			continue;
		// 加寫鎖
		write_lock_irq(&fa->fa_lock);
		// 更新文件描述符
		// 這裏不太理解的是:文件結構沒變,文件描述符會發生變化麼?
		fa->fa_fd = fd;
		// 釋放寫鎖
		write_unlock_irq(&fa->fa_lock);
		goto out;
	}
	// 初始化struct fasync_struc結構
	rwlock_init(&new->fa_lock);
	new->magic = FASYNC_MAGIC;
	new->fa_file = filp;
	new->fa_fd = fd;
	// 頭插式插入鏈表
	new->fa_next = *fapp;
	rcu_assign_pointer(*fapp, new);
	// 將文件結構的f_flags置FASYNC
	filp->f_flags |= FASYNC;

out:
	// 在退出之前需要將之前加的鎖解鎖
	spin_unlock(&fasync_lock);
	spin_unlock(&filp->f_lock);
	return fa;
}

3.1.4 總結

當我們在應用層調用fcntl(fd, F_SETFL, flag | FASYNC)時,驅動層會爲我們申請一個struct fasync_struct類型的變量,並將這個變量以頭插的方式插入到有struct fasync_struct類型的變量構成的鏈表中,不妨稱之爲異步通知鏈表。該鏈表中有很多項,每一項的存在都說明有一個進程使能了異步通知機制,即設置了FASYNC標誌(本質上是進程向異步通知鏈表插入了一個struct fasync_struct,這個結構中包含“如何找到進程”的信息)。

當然,要向異步通知鏈表插入本進程的struct fasync_struct,我們得提前申請好一個struct fasync_struct類型的指針,這個指針將指向上述鏈表頭,一般我們把這個指針放在設備結構體中(一般會爲要驅動的設備定義一個設備結構體):
在這裏插入圖片描述

3.2 驅動怎麼發送信號給進程——kill_fasync

到這裏,我們對異步通知應該有了更多的瞭解,在開始本節的分析之前,不妨再來總結一下異步通知機制

假設進程使用異步通知的方式來訪問設備A,那麼通過設備A的設備文件將自身註冊到設備A的異步通知鏈表之後,就可以幹其他事情了。設備A的驅動會在適當的時機向該設備的異步通知鏈表中(間接)記錄的每個進程發信號,而進程則在接收到信號之後運行綁定的信號處理函數,以完成對設備A的訪問。

那麼問題來了,什麼時機稱得上適當的時機呢?舉例來說明,比如:有進程向設備文件寫入了數據,此時設備有數據可讀,因此我們可以在驅動程序xxx_write中,寫入數據之後的地方調用kill_fasync(異步通知鏈表, SIGIO, POLL_IN)

接下來我們就仔細看看kill_fasync究竟做了什麼:

3.2.1 kill_fasync

void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
	// 設備的異步通知鏈表不爲空
	if (*fp) {
		// 獲取RCU讀鎖
		rcu_read_lock();
		// 調用kill_fasync_rcu發信號(在此之前要獲取RCU讀鎖)
		kill_fasync_rcu(rcu_dereference(*fp), sig, band);
		// 釋放RCU讀鎖
		rcu_read_unlock();
	}
}

3.2.2 kill_fasync_rcu

static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
	// 遍歷設備的異步通知鏈表
	while (fa) {
		struct fown_struct *fown;
		// fasync_insert_entry函數在初始化struct fasync_struct的時候會給它的magic成員賦值爲FASYNC_MAGIC
		// 也就是說如果magic成員不是FASYNC_MAGIC,那麼說明struct fasync_struct沒有被正確的初始化
		if (fa->magic != FASYNC_MAGIC) {
			printk(KERN_ERR "kill_fasync: bad magic number in "
			       "fasync_struct!\n");
			return;
		}
		// 加讀鎖
		read_lock(&fa->fa_lock);
		if (fa->fa_file) {
			// 獲取設備文件的(文件結構的)f_owner字段
			// 上文已經說了,該字段記錄了進程的pid
			// 該字段還有一個非常重要的成員:signum
			// fcntl(fd, F_SETSIG, SIGIO)所做的正是將struct file的f_owner字段的signum賦值爲SIGIO(詳見fcntl.c的do_fcntl函數)
			fown = &fa->fa_file->f_owner;
			
			// 當我們在驅動中調用kill_fasync(異步通知鏈表, SIGXXX, band)
			// SIGXXX會被傳遞給這裏的sig
			// 在未設置f_owner的signum時,我們不能發送SIGURG:SIGURG有它自己默認的處理機制
			if (!(sig == SIGURG && fown->signum == 0))
				send_sigio(fown, fa->fa_fd, band);
		}
		// 釋放讀鎖
		read_unlock(&fa->fa_lock);
		
		// 遍歷下一項struct fasync_struct
		fa = rcu_dereference(fa->fa_next);
	}
}

3.2.3 send_sigio

這個函數應該在介紹Linux的信號的時候分析更爲合適,當然異步通知機制是建立在Linux的信號機制的基礎上的,因此我仍然會跟蹤一下這個函數,但不會特別深入的分析,等深入學習Linux的信號機制時,再對其做更深入的介紹吧!

在分析源碼之前,我們先了解一下Linux的pid_type

enum pid_type
{
	PIDTYPE_PID,	// pid
	PIDTYPE_TGID,	// 線程組ID
	PIDTYPE_PGID,	// 進程組ID
	PIDTYPE_SID,	// 會話組ID
	PIDTYPE_MAX,	// 共4種類型
};

這部分內容和Linux的進程管理有關係,我在網上找了一篇博客[3],可以參考其中內容,以加深對這部分知識的理解。爲不偏離主線,這裏不再多說。下面就開始分析send_sigio函數:

void send_sigio(struct fown_struct *fown, int fd, int band)
{
	struct task_struct *p;
	enum pid_type type;
	struct pid *pid;
	// 獲取讀鎖
	read_lock(&fown->lock);
	// 獲取pid_type
	type = fown->pid_type;
	// 獲取指向struct pid的指針
	// 這個指針指向的struct pid會告訴我們將信號發給哪個進程
	pid = fown->pid;
	if (!pid)
		goto out_unlock_fown;
	
	// pid_type不同,所做的處理也不同,但都是調用了send_sigio_to_task
	// 我們不妨認爲進入的是眼下這個分支
	if (type <= PIDTYPE_TGID) {
		rcu_read_lock();
		// 從pid獲取相應進程的task_struct
		p = pid_task(pid, PIDTYPE_PID);
		if (p)
			// p指向信號的目的進程(一般就是打開該設備文件的那個進程)
			// fown指向設備文件的文件結構的f_owner字段
			// fd是我們打開的設備文件的文件描述符
			// band由kill_fasync傳入,可以是POLL_IN、POLL_OUT等(注意不是POLLIN、POLLOUT)
			// type記錄了pid_type
			send_sigio_to_task(p, fown, fd, band, type);
		rcu_read_unlock();
	} else {
		read_lock(&tasklist_lock);
		do_each_pid_task(pid, type, p) {
			send_sigio_to_task(p, fown, fd, band, type);
		} while_each_pid_task(pid, type, p);
		read_unlock(&tasklist_lock);
	}
 out_unlock_fown:
 	// 釋放讀鎖
	read_unlock(&fown->lock);
}

3.2.4 send_sigio_to_task

static void send_sigio_to_task(struct task_struct *p, struct fown_struct *fown, int fd, int reason, enum pid_type type)
{
	// 獲取設備文件的文件結構的f_owner字段的signum
	// 回憶一下,之前說過signum是在fcntl(fd, F_SETSIG, SIGXXX)被設置爲SIGXXX
	int signum = READ_ONCE(fown->signum);
	
	// 沒看明白,不清楚是做什麼的(猜測是校驗能否向p指向的進程發送信號signum)
	if (!sigio_perm(p, fown, signum))
		return;

	// 由signum來決定要做哪些事情
	switch (signum) {
		kernel_siginfo_t si;
		default:
			// 填充kernel_siginfo_t,這個結構裏的信息應該是傳給應用層的siginfo_t
			clear_siginfo(&si);
			si.si_signo = signum;
			si.si_errno = 0;
		    si.si_code  = reason;
			
			if ((signum != SIGPOLL) && sig_specific_sicodes(signum))
				si.si_code = SI_SIGIO;

			// 確保reason是POLL_XXX,否則可能會泄漏內核棧到用戶空間(爲什麼會泄露呢?不明白)
			BUG_ON((reason < POLL_IN) || ((reason - POLL_IN) >= NSIGPOLL));
			if (reason - POLL_IN >= NSIGPOLL)
				si.si_band  = ~0L;
			else
				si.si_band = mangle_poll(band_table[reason - POLL_IN]);
			si.si_fd    = fd;
			
			// 調用do_send_sig_info發送信號
			if (!do_send_sig_info(signum, &si, p, type))
				break;
		
		// 如果沒有設置signum的話,就調用do_send_sig_info發送SIGIO信號
		case 0:
			do_send_sig_info(SIGIO, SEND_SIG_PRIV, p, type);
	}
}

3.2.5 總結並提出自己的一點疑惑

kill_fasync 的調用棧總結如下(跟蹤的比較淺):
在這裏插入圖片描述

其中,令我感到困惑的是kill_fasyncsig參數和signum之間的關係。比如我們在驅動xxx_write中調用kill_fasync(異步通知鏈表, SIGIO, POLL_IN),難道不是表明我們想發送SIGIO信號嗎?如果是,那麼可以看到,在kill_fasync_rcu中,sig參數只要不是SIGURG就不會起到作用,而真正起作用的是signum,即我們通過fcntl(fd, F_SETSIG, SIGXXX)設置的字段。這樣豈不是說,我們在應用層就已經決定了kill_fasync將要發什麼信號?這個地方暫時還無法理解,可能需要對Linux的信號機制有更多的認識吧。這裏先將不解之處記錄下來,如果有知道答案的朋友,希望不吝賜教#_#!

3.3 文件關閉時要做的清理工作

當我們結束對設備的訪問時,通常會關閉設備的設備文件,關閉操作(close)在設備驅動層對應的是struct file_operations的release成員,一般會在驅動中實現xxx_release,並將其註冊到struct file_operations的release成員。

關閉設備文件時,通常要做一些清理工作,清理一些在open操作及之後申請的資源。回顧一下,我們使用異步通知的時候,進程設置FASYNC標誌,進而申請了一個struct fasync_struct類型的變量,這個變量需要在關閉操作中釋放,否則會造成內存泄露。

怎麼去釋放呢?很簡單,我們只要在xxx_release中調用xxx_fasync(-1, file, 0)需要注意的是資源釋放的順序往往不是隨意的。至於xxx_fasync(-1, file, 0)究竟做了什麼,這個很簡單,看一下源碼就知道,它其實是調用了fasync_remove_entry來移除並釋放相應的struct fasync_struct,這裏不再細說。

4 參考文獻

[1] 韋東山老師視頻教程一期
[2] linux-kernel-5.3.1
[3] Linux 內核進程管理之進程ID

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章