目錄
前言
本文主要是我學習Linux的異步通知機制所做的筆記,介紹了異步通知的一些應用層和驅動層的接口,內容會不斷的完善。
所涉及到的源碼的kernel版本是linux-kernel-5.3.1
1 概覽異步通知
當我們訪問文件的時候,比如讀/寫,如果當前不能讀或不能寫,那麼我們可以選擇阻塞訪問,直到有數據可讀或有空間可寫。而異步通知是進程先打開設備文件,再通過fcntl函數,設置F_SETOWN、FASYNC標誌,從而告知相應的設備:想要訪問你的進程具體是哪個,且想要以異步通知的方式訪問你。起到的效果是可讀/可寫的時候,驅動層會給進程發信號,進程收到信號之後執行相應的處理函數,在處理函數中進行讀/寫操作。進程不需要管什麼時候文件是可訪問的,就像你點了外賣以後,不需要死等,你只要接着幹自己的事情就行,外賣小哥到了會給你打電話(發信號)的。
我們以一幅圖來概括一下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_operations
的fasync
成員,因此我們需要在驅動層實現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 *fasync
。struct 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_fasync
的sig
參數和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