阻塞進程
當某人要求你什麼事而你當時不能時你在做什麼?如果你是人而你被別人打擾,你唯一能說的是:‘現在不行,我正忙著呢。 走開!’。但是如果你是一個內核模塊而你被一個進程打擾,你有另外的可能。你可以讓那個進程睡眠直到你能為它服務。畢竟,內核可以讓進程睡眠並且可以隨時喚醒它(那就是在單CPU上呈現同一時間多個進程運行的方式)。
這個內核模塊就是這樣的例子。那個文件(被稱為 /proc/sleep)在同一時間只能被一個進程打開。如果那個文件已經打開了,內核模塊調用module_interruptible_sleep_on(保持一個文件打開的最簡單的辦法是用 tail -f)。這個函數改變那個任務(任何任務是包含有關進程的信息和系統調用的內核的一種數據結構)的狀態為TASK_INTERRUPTIBLE,它的意思是任務不能運行,除非它被喚醒。並且該任務被加入 WaitQ-- 等待訪問該文件的任務隊列。然後函數調用調度程序進行上下文轉換到一個還要使用CPU的不同的進程。
當一個進程用完該文件,它關閉該文件,然後module_close 被調用。那個函數喚醒隊列中的所有進程(沒有機制只喚醒其中的一個)。然後它返回而剛剛關閉該文件的進程可以繼續運行。調度程序及時地決定那個進程已經用了夠多的時間而將CPU的控制權交給另一個進程。最後,隊列中的某個進程會獲得調度程序賦予的CPU的控制權。它正好在對module_interruptible_sleep_on(這意味著進程仍然在內核模式--直到進程被關照,它發布 open 系統調用然而系統調用還沒有返回。進程不知道別人在它發布調用和它返回之前的大部分時間內使用CPU)的調用後開始。然後它能繼續設置一個全局變量以告訴所有其他進程該文件仍然打開,它們將繼續它們等待的生活。當另一個進程得到CPU時間片,它們將看到那個全局變量而繼續去睡眠。
為了使我們的生活更有趣, module_close 沒有喚醒等待訪問該文件的進程的壟斷權。一個信號,例如Ctrl-C (SIGINT)也可以喚醒一個進程(這是因為我們使用 module_interruptible_sleep_on。我們不能使用module_sleep_on 作為代替,但那將導致那些他們控制的計算機被忽略的用戶的極度的憤怒)。在那種情況下,我們想立即用 -EINTR 返回。這是很重要的,例如用戶因此可以在進程收到該文件前將它殺死。
還有一點需要記住。有時候進程不想睡眠,它們想要麼立即得到它們需要的要麼被告知它不能完成。當文件打開時這類進程使用 O_NONBLOCK 標志。例如當文件打開時,內核被假設從阻塞操作中返回錯誤代碼-EAGAIN作為回應。在這章的源目錄中有的程序 cat_noblock 能用O_NONBLOCK打開文件。
范例 sleep.c
/* sleep.c - 創建一個 /proc 文件,而如果幾個進程同時試圖打開它,除了一個外使其他所有的睡眠 */ /* Copyright (C) 1998-99 by Ori Pomerantz */ /* 必要頭文件 */ /* 標准頭文件 */ #include /* 內核工作 */ #include /* 明確指定是模塊 */ /* 處理 CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif /* 使用 proc 文件系統所必須的 */ #include /* 為了使進程睡眠和喚醒 */ #include #include /* 2.2.3 版/usr/include/linux/version.h 包含該宏 * 但 2.0.35 版不包含--假如以備需要 */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include /* 為了得到 get_user 和 put_user */ #endif /* 模塊的文件函數 ********************** */ /* 保存收到的上一個消息以證明能夠處理輸入 */ #define MESSAGE_LENGTH 80 static char Message[MESSAGE_LENGTH]; /* 既然使用文件操作結構,我們就不能特殊的 proc 輸出 - 我們不得不使用這個標准的讀函數 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static ssize_t module_output( struct file *file, /* 要讀的文件 */ char *buf, /* 放置數據的緩沖區(在用戶內存段) */ size_t len, /* 緩沖區長度 */ loff_t *offset) /* 文件偏移量 - 忽略 */ #else static int module_output( struct inode *inode, /* 要讀的節點 */ struct file *file, /* 要讀的文件 */ char *buf, /* 放置數據的緩沖區(在用戶內存段) */ int len) /* 緩沖區長度 */ #endif { static int finished = 0; int i; char message[MESSAGE_LENGTH+30]; /* 返回0指明文件尾--沒有更多說的 */ if (finished) { finished = 0; return 0; } /* 如果你到現在還不懂這個,你沒有希望成為內核程序員 */ sprintf(message, "Last input:%s\n", Message); for(i=0; i put_user(message[i], buf+i); finished = 1; return i; /* 返回‘讀’的字節數 */ } /* 當用戶向 /proc 文件寫說這個函數接收用戶輸入 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static ssize_t module_input( struct file *file, /* 文件自己 */ const char *buf, /* 帶有輸入的緩沖區 */ size_t length, /* 緩沖區長度 */ loff_t *offset) /* 文件偏移量-忽略 */ #else static int module_input( struct inode *inode, /* 文件的節點 */ struct file *file, /* 文件自己 */ const char *buf, /* 帶有輸入的緩沖區 */ int length) /* 緩沖區長度 */ #endif { int i; /* 將輸入放入 Message, module_output 隨後將能使用它 */ for(i=0; i #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(Message[i], buf+i); #else Message[i] = get_user(buf+i); #endif /* 我們需要一個標准的以0終止的字符串 */ Message[i] = '\0'; /* 返回使用過的輸入的字符數 */ return i; } /* 如果文件當前被某人打開則為1 */ int Already_Open = 0; /* 需要我們的文件的進程隊列 */ static struct wait_queue *WaitQ = NULL; /* 當 /proc 文件被打開時被調用 */ static int module_open(struct inode *inode, struct file *file) { /* 如果文件的標志包含 O_NONBLOCK 則意味著進程不想等待該文件。 * 在這種情況下,如果文件已經打開,我們將用返回 -EAGAIN 表示失敗,意思是“你將再試一次” * 而不讓寧願保持醒狀態的進程阻塞。 */ if ((file->f_flags & O_NONBLOCK) && Already_Open) return -EAGAIN; /* 這是放置 MOD_INC_USE_COUNT 的合適的位置,因為如果一個進程處於內核模塊的循環中, * 內核模塊不應被清除。 */ MOD_INC_USE_COUNT; /* 如果文件已經打開,等待,直到它不再如此 */ while (Already_Open) { #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) int i, is_sig=0; #endif /* 該函數使當前的進程睡眠,包括任何系統調用,例如我們的。 * 執行將在函數調用後恢復,要麼因為某人調用了 wake_up(&WaitQ) (只有當文件被關閉時 * 由 module_close 做這個),要麼諸如 Ctrl-C 之類的信號發送到進程 */ module_interruptible_sleep_on(&WaitQ); /* 如果因為得到一個信號我們醒來,我們不再阻塞,返回 -EINTR (系統調用失敗)。 * 這允許進程被殺死或停止。 */ /* * Emmanuel Papirakis: * * 在 2.2.* 上有一點更新。信號現在被包含在一個雙字中(64 位) 並被存儲在一個 * 包含雙無符號長整型數組的結構中。必要的話我們需要做兩次檢查。 * * Ori Pomerantz: * * 沒有人向我許諾他們決不使用多於64位,或者這本書不在16位字長的 Linux 版本上使用。 * 這些代碼無論如何都可以工作。 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) for(i=0; i<_NSIG_WORDS && !is_sig; i++) is_sig = current->signal.sig[i] & ~current->blocked.sig[i]; if (is_sig) { #else if (current->signal & ~current->blocked) { #endif /* 在這兒放置MOD_DEC_USE_COUNT 是很重要的,因為對於進程打開操作被中斷的地方永遠相應關閉 * 如果我們不在這兒減小使用計數,我們將留下一個正的使用計數而他沒有辦法降為0而 * 給我們一個不朽的只能通過重新啟動機器殺死的模塊。 */ MOD_DEC_USE_COUNT; return -EINTR; } } /* 如果我們到這兒, Already_Open 必須為0 */ /* 打開文件 */ Already_Open = 1; return 0; /* 允許訪問 */ } /* 當 /proc 文件被關閉時調用 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) int module_close(struct inode *inode, struct file *file) #else void module_close(struct inode *inode, struct file *file) #endif { /* 將 Already_Open 設為0,所以等待隊列 WaitQ 中的某個進程能將 Already_Open設回1以打開文件。 * 所有的其他進程在 Already_Open 為1時被調用而它們將回去繼續睡眠。 */ Already_Open = 0; /* 喚醒等待隊列 WaitQ中的所有進程,所以如果某個正在等待該文件,它們能有機會。 */ module_wake_up(&WaitQ); MOD_DEC_USE_COUNT; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) return 0; /* success */ #endif } /* 這個函數決定是否允許一個操作(返回0表示允許,返回非零數用於指明為什麼不允許)。 * * 操作可以是下面的某個值: * 0 - 執行(運行“文件” - 在我們的例子中沒有意義) * 2 - 寫(向內核輸入) * 4 - 讀(從內核輸出) * * 這是檢查文件權限的實際的函數。用 ls -l 返回的權限只作參考並且可以被忽略。 */ static int module_permission(struct inode *inode, int op) { /* 允許任何人從模塊中讀,但只有 root (uid 為0) 可以寫 */ if (op == 4 || (op == 2 && current->euid == 0)) return 0; /* 如果是其他任何值,訪問被拒絕。 */ return -EACCES; } /* 作為 /proc 文件登記的結構,包含所有相關函數指針 */ /* 對我們的 proc 文件的文件操作。這是放置當某人試圖對我們的文件做什麼時將被調用的函數的指針的地方 * NULL 意味著我們不想處理某些事。 */ static struct file_operations File_Ops_4_Our_Proc_File = { NULL, /* lseek */ module_output, /* 從文件“讀” */ module_input, /* 向文件“寫” */ NULL, /* 讀目錄 */ NULL, /* 選擇 */ NULL, /* ioctl */ NULL, /* mmap */ module_open,/* 當 /proc 文件被打開時調用 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) NULL, /* 刷新 */ #endif module_close /* 當它被關閉時調用 */ }; /* 對我們的proc文件的節點操作。我們需要它,因此我們將在某個地方指明我們想使用的文件操作結構及 * 用於權限的函數。也可能指定對節點進行操作的任何其他函數(我們不關心但放置 NULL)。 */ static struct inode_operations Inode_Ops_4_Our_Proc_File = { &File_Ops_4_Our_Proc_File, NULL, /* 創建 */ NULL, /* 查找 */ NULL, /* 連接 */ NULL, /* 刪除連接 */ NULL, /* 符號連接 */ NULL, /* 建目錄 */ NULL, /* 刪除目錄 */ NULL, /* mknod */ NULL, /* 更名 */ NULL, /* 讀連接 */ NULL, /* 跟隨連接 */ NULL, /* 讀頁 */ NULL, /* 寫頁 */ NULL, /* bmap */ NULL, /* 截短 */ module_permission /* 權限檢查 */ }; /* 目錄項 */ static struct proc_dir_entry Our_Proc_File = { 0, /* 節點數-忽略,將被 proc_register[_dynamic] 填充*/ 5, /* 文件名長度 */ "sleep", /* 文件名 */ S_IFREG | S_IRUGO | S_IWUSR, /* 文件模式-這是一個可以被其擁有者,用戶組及其他人讀的普通。當然其擁有者也可以寫。 * * 實際上這個成員只用於引用,它的 module_permission 進行實際的檢查。 * 它可以使用這個成員,但在我們的實現中為了簡單而沒有用。 */ 1, /* 連接數 (文件被引用的目錄) */ 0, 0, /* 文件的UID和GID - 賦予 root */ 80, /* 由 ls 報告的文件長度。 */ &Inode_Ops_4_Our_Proc_File, /* 如果需要則是文件節點結構的指針。在我們的實現中我們需要一個寫函數。 */ NULL /* 文件的讀函數。不相關,我們已經將它放置在上面的節點結構中。 */ }; /* 初始化和清除模塊 **************** */ /* 初始化模塊 - 登記 proc 文件 */ int init_module() { /* 如果 proc_register_dynamic 成功則成功,否則失敗 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) return proc_register(&proc_root, &Our_Proc_File); #else return proc_register_dynamic(&proc_root, &Our_Proc_File); #endif /* proc_root 是 proc 文件系統的根目錄(/proc)。這是我們想將我們的文件放置的地方。 */ } /* 清除 - 從 /proc 中注銷文件。如果在 WaitQ 中仍然有進程這將變得危險,因為它們在我們的打開函數裡 * 而它將不能被卸載。我將在第10章裡面解釋在這樣的情況下如何避免移除內核模塊。*/ void cleanup_module() { proc_unregister(&proc_root, Our_Proc_File.low_ino); }