您當前位置:
首頁 >
互聯網 > 《Linux Device Drivers》第六章 高級字符驅動程序操作――note
《Linux Device Drivers》第六章 高級字符驅動程序操作――note
來源:程序員人生 發布時間:2014-10-11 08:00:01 閱讀次數:2220次
- ioctl
- 支持的操作,例如
- 簡單數據傳輸
- 控制動作,例如用戶空間發起彈出介質動作
- 反饋硬件的狀態,例如報告錯誤信息
- 參數配置,例如改變波特率
- 執行自破壞
- 用戶空間的ioctl方法原型:int ioctl(int fd, unsigned long cmd, …);每個ioctl命令就是一個獨立的系統調用,而且是非公開的
- 驅動程序的ioctl方法原型:int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
- 選擇ioctl命令
- 為方便程序員創建唯一的ioctl命令號,每一個命令號被分為多個位字段
- Linux內核的約定方法為驅動程序選擇ioctl編號
- include/asm/ioctl.h
- Documentation/ioctl-number.txt
- <linux/ioctl.h>
- type
- 幻數,這個字段有8位寬(_IOC_TYPEBITS)
- number
- direction
- _IOC_NONE(沒有數據傳輸)
- _IOC_READ
- _IOC_WRITE
- _IOC_READ | _IOC_WRITE(雙向傳輸數據)
- size
- 所涉及的用戶數據大小
- 通常是13位或14位
- _IOC_SIZEBITS
- <asm/ioctl.h>
- _IO(type, nr)
- _IOR(type, nr, datatype)
- _IOW(type, nr, datatype)
- _IOWR(type, nr, datatype)
- _IOC_DIR(nr)
- _IOC_TYPE(nr);
- _IOC_NR(nr)
- _IOC_SIZE(nr);
- 返回值
- ioctl的實現通常就是一個基于命令號的switch語句
- 不能匹配任何合法的操作?
- 有些內核函數會返回-EINVAL
- POSIX標準規定,如果使用了不合適的ioctl命令參數,應該返回-ENOTTY
- 預定義命令
- 預定義命令分為三組
- 可用于任何文件(普通、設備、FIFO和套接字)的命令
- 只用于普通文件的命令
- 特定于文件系統類型的命令
- 設備驅動程序開發人員只對第一組感興趣,它們的幻數都是“T”
- FIOCLEX
- FIONCLEX
- FIOASYNC
- 設置或復位文件異步通知
- 這兩個動作都可以通過fcntl完成,實際上沒有人會使用FIOASYNC
- FIOQSIZE
- FIONBIO
- 使用ioctl參數
- <asm/uaccess.h>
- int access_ok(int type, const void *addr, unsigned long size);
- type
- addr
- size
- 返回一個布爾值:1表示成功,0表示失敗
- 如果返回失敗,驅動程序通常要返回-EFAULT給調用者
- put_user(datum, ptr);
- __put_user(datum, ptr);
- get_user(local, ptr);
- __get_user(local, ptr);
- 權能與受限操作
- 基于權能(capability)的系統把特權操作劃分為獨立的組
- capget
- capset
- <linux/capability.h>
- CAP_DAC_OVERRIDE
- CAP_NET_ADMIN
- CAP_SYS_MODULE
- CAP_SYS_RAWIO
- CAP_SYS_ADMIN
- CAP_SYS_TTY_CONFIG
- <sys/sched.h>
- int capable(int capability);
- 阻塞型I/O
- 休眠的簡單介紹
- 當一個進程被置入休眠時,它會被標記為一種特殊狀態并從調度器的運行隊列中移走
- 永遠不要在原子上下文中進入休眠
- 如果代碼在擁有信號量時休眠,任何其他等待該信號量的線程也會休眠,因此任何擁有信號量而休眠的代碼必須很短,并且還要確保擁有信號量并不會阻塞喚醒我們自己的那個進程
- 當我們被喚醒時,我們永遠無法知道休眠了多長時間,或者休眠期間都發生了些什么事情
- 在Linux中,一個等待隊列通過一個“等待隊列頭(wait queue head)”來管理
- <linux/wait.h>
- wait_queue_head_t
- DECLARE_WAIT_QUEUE_HEAD(name);
- wait_queue_head_t my_queue;
- init_waitqueue_head(&my_queue);
- 簡單休眠
- wait_event(queue, condition);
- wait_event_interruptible(queue, condition);
- wait_event_timeout(queue, condition, timeout);
- wait_event_interruptible_timeout(queue, condition, timeout);
- void wake_up(wait_queue_head_t *queue);
- void wake_up_interruptible(wait_queue_head_t *queue);
- 阻塞和非阻塞型操作
- 顯式的非阻塞I/O由filp->f_flags中的O_NONBLOCK標志決定
- <linux/fcntl.h>
- 在執行阻塞型操作的情況下,應該實現下列動作以保持和標準語義一致
- 如果一個進程調用了read但是還沒有數據可讀,此進程必須阻塞
- 如果一個進程調用了write但緩沖區沒有空間,此進程必須阻塞,而且必須休眠在與讀取進程不同的等待隊列上
- 高級休眠
- 進程如何休眠
- 將進程置于休眠的第一個步驟通常是分配并初始化一個wait_queue_t結構,然后將其加入到對應的等待隊列
- 第二個步驟是設置進程的狀態,將其標記為休眠
- <linux/sched.h>
- TASK_RUNNING
- TASK_INTERRUPTIBLE
- TASK_UNINTERRUPUTIBLE
- void set_current_state(int new_state);
- 放棄處理器是最后的步驟,但在此之前還要做另外一件事情:我們必須首先檢查休眠等待的條件
- if (!condition) schedule();
- 手工休眠
- <linux/sched.h>
- DEFINE_WAIT(my_wait);
- wait_queue_t my_wait;
- init_wait(&my_wait);
- void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
- state是進程的新狀態,應該是TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
- schedule();
- void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
- 獨占等待
- 當某個進程在等待隊列上調用wake_up時,所有等待在該隊列上的進程都被置為可運行狀態
- 只會有一個被喚醒的進程可以獲得期望的資源,而其他被喚醒的進程只會再次休眠
- 一個獨占等待的行為和通常的休眠類似,但有如下兩個重要的不同
- 等待隊列入口設置了WQ_FLAG_EXCLUSIVE標志時,則會被添加到等待隊列的尾部
- 在某個等待隊列上調用wake_up時,它會在喚醒第一個具有WQ_FLAG_EXCLUSIVE標志的進程之后停止喚醒其他進程
- 如果滿足下面兩個條件,在驅動程序中利用獨占等待是值得考慮的
- 對某個資源存在嚴重競爭,并且喚醒單個進程就能完整消耗該資源
- void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
- 喚醒的相關細節
- <linux/wait.h>
- wake_up(wait_queue_head_t *queue);
- 喚醒隊列上所有非獨占等待的進程,以及單個獨占等待者
- wake_up_interruptible(wait_queue_head_t *queue);
- wake_up_nr(wait_queue_head_t *queue, int nr);
- wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);
- wake_up_all(wait_queue_head_t *queue);
- wake_up_interruptible_all(wait_queue_head_t *queue);
- wake_up_interruptible_sync(wait_queue_head_t *queue);
- 舊的歷史:sleep_on
- void sleep_on(wait_queue_head_t *queue);
- void interruptible_sleep_on(wait_queue_head_t *queue);
- 永遠不要使用它們
- poll和select
- poll、select和epoll系統調用
- poll、select和epoll的功能本質上是一樣的:都允許進程決定是否可以對一個或多個打開的文件做非阻塞的讀取或寫入
- select在BSD Unix中引入
- poll由System V引入
- unsigned int (*poll) (struct file *filp, poll_table *wait);
- poll_table結構,用于在內核中實現poll、select及epool系統調用
- <linux/poll.h>
- void poll_wait(struct file *, wait_queue_head_t *, poll_table *);
- poll方法執行的第二項任務是返回描述哪個操作可以立即執行的位掩碼
- <linux/poll.h>
- POLLIN
- POLLRDNORM
- 如果“通常”的數據已經就緒,可以讀取,就設置該位
- 一個可讀設備返回(POLLIN|POLLRDNORM)
- POLLRDBAND
- 這一位指示可以從設備讀取out-of-band的數據
- POLLPRI
- POLLHUP
- 當讀取設備的進程到達文件尾時,驅動程序必須設置POLLHUP位
- POLLERR
- POLLOUT
- POLLWRNORM
- 該位和POLLOUT的意義一樣,有時其實就是同一個數字
- 一個可寫的設備將返回(POLLOUT|POLLWRNORM)
- POLLWRBAND
- 與POLLRDBAND類似,這一位表示具有非零優先級的數據可以被寫入設備
- POLLRDBAND和POLLWRBAND只在與套接字相關的文件描述符中才是有意義的,設備驅動程序通常用不到這兩個標志
- 與read和write的交互
- 從設備讀取數據
- 如果輸入緩沖區有數據,那么即使就緒的數據比程序所請求的少,并且驅動程序保證剩下的數據馬上就能到達,read調用仍然應該以難以察覺的延遲立即返回
- 如果緩沖區中沒有數據,那么默認情況下read必須阻塞等待,直到至少有一個字節到達;如果設置了O_NONBLOCK標志,read應立即返回,返回值是-EAGAIN。poll必須報告設備不可讀
- 如果已經到達文件尾,read應該立即返回0,此時poll應該報告POLLHUP
- 向設備寫數據
- 如果輸出緩沖區中有空間,則write應該無延遲地立即返回,在這種情況下,poll報告設備可寫
- 如果輸出緩沖區已滿,那么默認情況下write被阻塞直到有空間釋放;如果設置了O_NONBLOCK標志,write應立即返回,返回值是-EAGAIN。poll必須報告設備不可寫
- 永遠不要讓write調用在返回前等待數據的傳輸結束
- 刷新待處理輸出
- int (*fsync) (struct file *file, struct dentry *dentry, int datasync);
- 如果應用程序需要確保數據已經被傳送到設備上,就必須fsync方法
- datasync用于區分fsync和fdatasync這兩個系統調用
- 底層的數據結構
- poll_table結構是構成實際數據結構的一個簡單封裝,包含poll_table_entry結構的內存頁鏈表
- 每個poll_table_entry結構包括一個指向被打開設備的struct file類型的指針、一個wait_queue_head_t指針以及一個關聯的等待隊列入口
- 如果輪詢(poll)時沒有一個驅動程序可以進行非阻塞I/O,這個poll調用者就進入休眠,直到休眠在其上的某個(或多個)等待隊列喚醒它為止
- poll實現中的珍上有趣之處是,驅動程序的poll方法在被調用者時為plol_table參數傳遞NULL指針。
- 在poll調用結束時,poll_table結構被重新分配,所有的先前添加到poll表中的等待隊列入口都會從這個表以及等待隊列中移除
- 異步通知
- 為了啟用文件袋異步通知機制,用戶程序必須執行兩個步驟
- 首先,它們指定一個進程作為文件的屬主,當進程使用fcntl系統調用執行F_SETOWN命令時,屬主進程的進程ID號就被保存在filp->f_owner中
- 然后,用戶程序必須在設備中設備FASYNC標志,通過fcntl的F_SETFL命令完成的
- 例子
- struct sigaction action;
- memset(&action, 0, sizeof(action));
- action.sa_handler = sighandler;
- action.sa_flags = 0;
- sigaction(SIGIO, &action, NULL);
- fcntl(STDIN_FILENO, F_SETOWN, getpid());
- oflags = fcntl(STDIN_FILENO, F_GETFL);
- fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
- 從驅動程序的角度考慮
- 從內核角度來看的詳細操作過程
- F_SETOWN被調用時對filp->f_owner賦值,此外什么也不做
- 在執行F_SETFL啟用FASYNC時,調用驅動程序的fasync方法,只要filp->f_flags中的FASYNC標志發生了變化,就會調用該方法,以便把這個變化通知驅動程序,使其能正確響應
- 當數據到達時,所有注冊為異步通知的進程都會被發送一個SIGIO信號
- <linux/fs.h>
- struct fasync_struct
- int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
- void kill_fasync(struct fasync_struct **fa, int sig, int band);
- sig通常是SIGIO
- band通常是POLL_IN,等價于POLLIN|POLLRDNORM
- 某些設備也針對設備可寫入而實現了異步通知,在這種情況下,kill_fasync必須以POLL_OUT為模式調用
- 當文件關閉時必須的調用fasync方法
- 定位設備
- llseek實現
- 如果設備操作未定義llseek方法,內核默認通過修改filp->f_pos而執行定位
- 如果定位操作對應于設備的一個物理操作,可能就需要提供自己的llseek方法
- 如果定位設備是沒有意義的,應該在open方法中調用nonseekable_open,通知內核設備不支持llseek
- int nonseekable_open(struct inode *inode, struct file *filp);
- 還應該將file_operations結構中的llseek方法設置為特殊的輔助函數no_llseek
- 設備文件的訪問控制
- 獨享設備
- 限制每次只由一個用戶訪問
- 需要兩個數據項
- current->uid
- current->euid
- 替代EBUSY的阻塞型open
- 當設備不能訪問時返回一個錯誤,通常這是最合理的方式,但有些情況下可能需要讓進程等待設備
- 在打開時復制設備
- 另一個實現訪問控制的方法是,在進程打開設備時創建設備的不同私有副本
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈
------分隔線----------------------------
------分隔線----------------------------