綜述
在本章中,我們要掌握以下知識點
ioctl接口的使用
如何使進程休眠并且喚醒
如何實現(xiàn)非阻塞I/O
在設備可寫入或者讀取時如何通知用戶空間
1. ioctl
ioctl函數(shù)解析
除了讀取和寫入設備之外鞠眉,大部分驅(qū)動程序還需要另外一種能力,即通過設備驅(qū)動程序執(zhí)行各種類型的硬件控制删掀,通過ioctl可以實現(xiàn)!
在用戶空間导街,ioctl系統(tǒng)調(diào)用原型
int ioctl(int f,unsigned long cmd,...);
第三個參數(shù)...代表可變參數(shù)
在內(nèi)核空間的ioctl
int (*ioctl) (strcut inode *inode,strcut file *filp
,unsigned int cmd,unsigned long arg);
inode和filp兩個指針的值對應于應用程序傳遞的文件描述符fd,和open方法是一樣的
參數(shù)cmd又用戶空間不經(jīng)修改地傳遞給驅(qū)動程序
可選參數(shù)arg:無論是用戶程序使用的是指針還是整數(shù)值纤子,都以unsigned long的形式傳遞給驅(qū)動程序
選擇ioctl命令
一般來說我們本能地會從0或者1開始編號搬瑰,但是這樣無法保證每個ioctl命令是唯一的,因此Linux給我們提供了自己的方案控硼。
定義命令號的方法使用了4個字段泽论,包含在頭文件<linux/ioctl.h>
type
幻數(shù)。選擇一個號碼卡乾,并在整個驅(qū)動程序中使用這個號碼翼悴,該字段有8位寬(_IOC_TYPEBITS)
number
序數(shù)(順序編號)。也是8位寬(_IOC_NRBITS).
derection
數(shù)據(jù)的傳輸方向幔妨,類型包括
_IOC_NONE(沒有數(shù)據(jù)傳輸)
_IOC_READ(讀取數(shù)據(jù))
_IOC_WRITE(寫入數(shù)據(jù))
_IOC_READ | _IOC_WRITE(雙向傳輸數(shù)據(jù)-可讀可寫)
注意:這里的數(shù)據(jù)傳輸是用應用程序的角度來看的鹦赎,因此
_IOC_READ代表從設備中讀取數(shù)據(jù),所以驅(qū)動程序必須向用戶空間寫入數(shù)據(jù)误堡。
size
用戶數(shù)據(jù)的大小古话,該字段寬度和體系結(jié)構(gòu)有關,通常是13-14位锁施,具體可以查找宏_IOC_SIZEBITS來確定陪踩,系統(tǒng)并不強制使用這個字段杖们!
構(gòu)造命令的宏
<linux/ioctl.h>包含<asm/ioctl.h>頭文件定義的已結(jié)構(gòu)造命令的宏
_IO(type,nr):構(gòu)造無參數(shù)的命令編號
_IOR(type,nr,datatype):構(gòu)造從驅(qū)動程序中讀取數(shù)據(jù)的命令編號
_IOW(type,nr,datatype):構(gòu)造往內(nèi)核空間寫入數(shù)據(jù)的命令編號
_IOWR(type,nr,datatype):構(gòu)造雙向數(shù)據(jù)傳輸?shù)拿罹幪?/p>
參數(shù):type和nr(number)通過參數(shù)傳入,而size字段通過對datatype參數(shù)取sizeof()獲得
解開位字段的宏
_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)
構(gòu)造IOCTL命令示例
/*使用k作為幻數(shù)*/
#define MY_IOC_MAGIC 'k'
/**在你自己的代碼中肩狂,請使用8位數(shù)字*/
/*
*S 表示設置(Set)-通過指針設置(Set)
*T 表示通知 (Tell)-直接使用參數(shù)值 通知 (Tell)
*G 表示獲取(Get)-通過設置指針來應答
*Q 表示查詢(Query)-通過返回值應答
*X 表示交換(eXchange)-原子的交換G和S
*H 表示切換(sHift)-原子的交換T和Q
*/
//自定義IOCTL命令 使用以上的宏
#define MY_IOC_UANTUM _IOW(MY_IOC_MAGIC,1,int);
#define MY_IOC_SQ_SET _IOW(MY_IOC_MAGIC,2,int);
#define MY_IOC_TQ_SET _IO(MY_IOC_MAGIC,3);
#define MY_IOC_SQ_GET _IOR(MY_IOC_MAGIC,4,int);
另一種定義命令的方式就是顯示地聲明一組數(shù)字摘完,但也會帶來很多問題,
頭文件<linux/kd.h>
就是這種舊風格的使用方式傻谁,使用了16位標量數(shù)值來定義ioctl命令孝治,這是因為那時候只有這種方式!Uっ荆秦!
返回值
ioctl的實現(xiàn)通常就是基于命令號的switch語句,若是未能匹配任何ioctl命令力图,默認返回-ENVAL(Invalid argument,非法參數(shù))步绸。
預定義命令
有些命令內(nèi)核已經(jīng)定義好了,因此我們定義的命令不能和內(nèi)核的沖突吃媒,否則就接收不到我們自己的命令瓤介。
預定義命令分為三組:
可用于任何文件(普通、設備赘那、FIFO和套接字)的命令
只用于普通文件的命令
特定文件系統(tǒng)類型的命令
設備驅(qū)動開發(fā)人員只對第一組感興趣刑桑,他們的幻數(shù)都是"T"
ext2_ioctl,實現(xiàn)了只追加標志(append-only)和不可變標志(immutable)
以下的ioctl命令對任何文件都是預定義的:
FIOCLEX
設置執(zhí)行時關閉標志(File IOctl CLose on Exec)募舟。設置了這個標志之后祠斧,當調(diào)用進程執(zhí)行一個新程序時,文件描述符將被關閉
FIONCLEX
清除執(zhí)行時關閉標志(File IOctl Not CLose on EXec)拱礁。該命令將恢復通常的文件行為琢锋,并撤銷上述FIOCLEX命令所做的工作。
FIOASYNC
設置或復位文件異步通知呢灶,注意知道Linux 2.2.4 內(nèi)核都不正確使用這個命令修改O_SYNC標志吴超。因為這兩個動作可以通過fcntl完成,所以實際上沒人使用這個命令鸯乃。
FIOQSIZE
該命令返回文件或者目錄的大小鲸阻。不過當用于設備文件時,會導致ENOTTY錯誤的返回
FIONBIO
"File IOctl Non-Blocking I/O",文件ioctl非阻塞型I/O缨睡。改調(diào)用秀阿貴filp->f_flags中的O_NONBLOCK標志鸟悴。傳遞系統(tǒng)調(diào)用的第三個參數(shù)指明了是設置還是清除該標準。修改該標志的常用方法是由fcntl系統(tǒng)調(diào)用使用F_SETFL命令來完成宏蛉。
使用ioctl參數(shù)
參數(shù)中指針合法檢測
注意遣臼,ioctl附加參數(shù),如果是整數(shù)可以直接使用拾并,如果是指針揍堰,當該指針指向用戶空間時鹏浅,要檢查該地址是否合法。
<asm/uaccess.h>
int access_ok(int type,const void *addr,unsigned long size);
用于驗證地址是否合法
type:VERIFY_READ或者VERIFY_WRITE(包含VERIFY_READ),取決于執(zhí)行動作是讀取還是寫入用戶空間屏歹。
addr:用戶空間地址
size:字節(jié)數(shù)
返回值:1-成功隐砸,0-失敗
注意2點:
1.access_ok沒有完成驗證內(nèi)存的全部工作,只是檢查了引用的內(nèi)存是否位于進程有對應訪問權限的區(qū)域內(nèi)蝙眶。
2.大多數(shù)驅(qū)動程序都沒必要調(diào)用access_ok季希,內(nèi)存管理程序會處理它。
代碼示例
方向是一個位掩碼幽纷,而VERUFY_WRITE用于R/W輸出
類型是真的用戶空間而言的式塌,而access_ok是面向內(nèi)核的
因此讀取和輸出恰好是相反的
if(_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE,(void __user *)arg,_IOC_SIZE(cmd));
用戶空間和內(nèi)核空間交換數(shù)據(jù)的方式
1.copy_from_user(),copy_to_user();
2.<asm/uaccess.h>
put_user(datum,ptr);
__put_user(datum,ptr);
這些宏把datum寫到用戶空間,速度快友浸。傳遞單個數(shù)據(jù)時使用該函數(shù)而不是copy_to_user();
__put_user(datum,ptr)比put_user(datum,ptr)做的檢查少一些峰尝,不調(diào)用access_ok,因此使用__put_user之前應該調(diào)用access_ok
get_user(local,ptr)
__get_user(local,ptr)
這些宏用于從用戶空間接收數(shù)據(jù)收恢,保存到locak中武学,除了傳輸方向不同,其他與put宏一樣伦意。
權能
#include <linux/capability.h>
定義了各種CAP_*符號火窒,用于描述用戶空間進程擁有的權能操作
int cap(int capability)
如果進程具有指定的權能,返回非零值驮肉。
2.如何使進程休眠并且喚醒
休眠含義
簡單的說熏矿,休眠是一種進程的特殊狀態(tài)(即task->state= TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)
休眠是為在一個當前進程等待暫時無法獲得的資源或者一個event的到來時(原因),避免當前進程浪費CPU時間(目的)离钝,將自己放入進程等待隊列中曲掰,同時讓出CPU給別的進程(工作)。休眠就是為了更好地利用CPU奈辰。
一旦資源可用或event到來,將由內(nèi)核代碼(可能是其他進程通過系統(tǒng)調(diào)用)喚醒某個等待隊列上的部分或全部進程乱豆。從這點來說奖恰,休眠也是一種進程間的同步機制。
休眠的規(guī)則
1.永遠不要在原子上下文中進入休眠
2.喚醒時必須檢測等待的條件真正為真
等待隊列
除非我們知道其他人會在某個地方喚醒休眠的進程宛裕,否則進程就不能進入休眠瑟啃,因此,Linux維護了一個稱為等待隊列的數(shù)據(jù)結(jié)構(gòu)(鏈表)揩尸。
在Linux中蛹屿,一個等待隊列通過"等待隊列頭(wati queue head)"來管理。
定義并初始化
#include <linux/wait.h>
靜態(tài)方法:
DEAKARE_WAIT_QUEUE_HEAD(name);
動態(tài)方法:
wait_queue_heat_t my_queue;
init_waitqueue_head(&my_queue);
休眠的方式
Linux中實現(xiàn)休眠最簡單的方式:wait_evet()以及他的變種
wait_event(queue,condition):非中斷休眠
wait_event_interruptible(queue,condition):可以被信號中斷休眠,返回值非零時表示休眠被某個信號中斷了岩榆。
wai_event_timeout(queue,condition,timeout)
不可中斷休眠错负,給定時間(以jiffy表示)到期時坟瓢,返回0,無論condition是否為真
wai_event_interruptible_timeout(queue,condition,timeout)
可中斷休眠犹撒,給定時間(以jiffy表示)到期時折联,返回0,無論condition是否為真
參數(shù):queue是指針隊列的頭识颊,通過值傳遞诚镰,而不是指針。
參數(shù):condition是任意一個布爾表達式祥款,該條件為真之前清笨,進程都會休眠。
喚醒的方式
void wake_up(wait_queur_head_t *queue):
喚醒所有進程
void wake_up_interruptible(wait_queue_head_t *queue);
喚醒可中斷進程
阻塞和非阻塞
有時候調(diào)用進程會通知我們它不想阻塞刃跛,無論I/O是否可以繼續(xù)抠艾。顯示的非阻塞I/O由filp->f_flags中的O_NONBLOCK標志決定,包含在<linux/fcntl.h>中奠伪,該頭文件又包含在<linux/fs.h>中跌帐,注意O_NDELAY和O_NONBLOCK是一樣的!
3.如何實現(xiàn)非阻塞I/O
通常在驅(qū)動程序內(nèi)部绊率,阻塞在read調(diào)用的進程在數(shù)據(jù)到達時被喚醒谨敛;通常硬件會發(fā)出一個中斷來通知這個數(shù)據(jù)到來的事件,然后作為中斷處理的一部分滤否,驅(qū)動程序會喚醒等待進程脸狸。不過我們要編寫的驅(qū)動不基于硬件,就沒有中斷藐俺,我們選擇使用另一個進程來產(chǎn)生數(shù)據(jù)并且喚醒讀取進程炊甲。類似的讀取進程用來喚醒等待緩沖區(qū)空間的寫入進程!
struct scull_pipe {
wait_queue_head_t inq,outq;//讀取和寫入隊列
char *buffer,*end;//緩沖區(qū)的起始和結(jié)尾
int buffersize;//用于指針計算
char *rp,*wp;//讀取和寫入的位置
int nreaders,nwriters;//讀取和寫入打開的數(shù)量
struct fasync_struct *async_queue;//異步讀取者
struct semaphore sem;//互斥信號量
struct cdev cdev;//字符設備結(jié)構(gòu)
}
read時序負責管理阻塞型和非阻塞型輸入
static ssize_t scull_p_read(struct file *filp,char __user *buf,size_t count,
loff_t *f_pos)
{
struct scull_pipe *dev = filep->private_data;
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
while(dev->rp = = dev->wp){//無數(shù)據(jù)可讀
up(&dev->sem);//釋放鎖
//檢查用戶請求的是否是非阻塞I/O欲芹,如果是卿啡,直接返回,否則進入休眠
if(filp->f_flags & O_NONBLOCK)
return -EAGAIN;
if(wait_event_interruptible(dev->inq,(dev->rp != dev->wp)))
return -ERESTARTSYS;//信號 通知fs層做相應處理
//否則循環(huán) 但首先獲取鎖
if(dowm_interruptible(&dev->sem))
return -ERESTARTSYS;
}
//數(shù)據(jù)已就緒
if(dev->wp > dev->rp)
count = min (count,(size_t)(dev->wp - dev->rp));
else
count = min(count,(size_t)(dev->end- dev->rp));
if(copy_to_user(buf,dev->rp,count)) {
up(&dev->sem);
return -EFAULT;
}
dev->rp += count;
if(dev->rp == dev->end)
dev->rp = dev->buffer;
up(&dev->sem);
//喚醒所有寫入者并返回
wake_up_interruptible(&dev->outq);
return count;
}
高級休眠
休眠步驟一:分配并且初始化一個wait_queue_t結(jié)構(gòu)菱父,然后將其加入等待隊列中颈娜。
休眠步驟二:設置進程狀態(tài),將其標志為休眠浙宜。(Linux 2.6中官辽,通常不需要驅(qū)動程序代碼來直接操作進程狀態(tài))
<linux/sched.h>
TASK_RUNNING:進程可運行
TASK_INTERRUPTIBLE:可中斷休眠狀態(tài)
TASK_UNINTERRUPTIBLE:不可中斷休眠狀態(tài)
設置進程狀態(tài)
void set_current_state(int new_state);
或者
current->state = TASK_INTERRUPTIBLE;
休眠步驟三:必須檢查休眠等待的條件,如果不作這個檢查粟瞬,可能會導致競態(tài)同仆。
if(!condition)
schedule();//該函數(shù)將調(diào)用調(diào)度器,讓出CPU
手工休眠
該方式不推薦使用裙品,僅僅做了解即可
獨占等待
了解即可
喚醒更多細節(jié)
wake_up(wait_queue_head_t *q)
喚醒q隊列上的所有非獨占等待的進程俗批,以及單個獨占等待者(如果存在)
wake_up_interruptible(wait_queue_head_t *q)
和以上一樣的工作俗或,只是它會跳過不可中斷休眠的那些進程。
wake_up_nr(wait_queue_head_t *q)
wake_up_interruptible_nr(wait_queue_head_t *q)
只會喚醒nr個獨占等待進程扶镀,注意nr=0時表示喚醒所有獨占等待進程
wake_up_all(wait_queue_head_t *q)
wake_up_interruptible_all(wait_queue_head_t *q)
上述形式函數(shù)蕴侣,不管是否執(zhí)行獨占等待,均喚醒它們
wake_up_interruptible_sync(wait_queue_head_t *q)