[讀書筆記]高級字符驅(qū)動程序(第六章)

綜述

在本章中,我們要掌握以下知識點
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)

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末臭觉,一起剝皮案震驚了整個濱河市昆雀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蝠筑,老刑警劉巖狞膘,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異什乙,居然都是意外死亡挽封,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門臣镣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辅愿,“玉大人,你說我怎么就攤上這事忆某〉愦” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵弃舒,是天一觀的道長癞埠。 經(jīng)常有香客問我,道長聋呢,這世上最難降的妖魔是什么苗踪? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮削锰,結(jié)果婚禮上通铲,老公的妹妹穿的比我還像新娘。我一直安慰自己器贩,他們只是感情好测暗,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著磨澡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪质和。 梳的紋絲不亂的頭發(fā)上稳摄,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音饲宿,去河邊找鬼厦酬。 笑死胆描,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的仗阅。 我是一名探鬼主播昌讲,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼减噪!你這毒婦竟也來了短绸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤筹裕,失蹤者是張志新(化名)和其女友劉穎醋闭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體朝卒,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡证逻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了抗斤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囚企。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖瑞眼,靈堂內(nèi)的尸體忽然破棺而出龙宏,到底是詐尸還是另有隱情,我是刑警寧澤负拟,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布烦衣,位于F島的核電站,受9級特大地震影響掩浙,放射性物質(zhì)發(fā)生泄漏花吟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一厨姚、第九天 我趴在偏房一處隱蔽的房頂上張望衅澈。 院中可真熱鬧,春花似錦谬墙、人聲如沸今布。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽部默。三九已至,卻和暖如春造虎,著一層夾襖步出監(jiān)牢的瞬間傅蹂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留份蝴,地道東北人犁功。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像婚夫,于是被迫代替她去往敵國和親浸卦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容