6.6. 在一個設(shè)備文件上的存取控制

2018-02-24 15:49 更新

6.6.?在一個設(shè)備文件上的存取控制

提供存取控制對于一個設(shè)備節(jié)點來說有時是至關(guān)重要的. 不僅是非授權(quán)用戶不能使用設(shè)備(由文件系統(tǒng)許可位所強加的限制), 而且有時只有授權(quán)用戶才應(yīng)當被允許來打開設(shè)備一次.

這個問題類似于使用 ttys 的問題. 在那個情況下, login 進程改變設(shè)備節(jié)點的所有權(quán), 無論何時一個用戶登錄到系統(tǒng), 為了阻止其他的用戶打擾或者偷聽這個 tty 的數(shù)據(jù)流. 但是, 僅僅為了保證對它的唯一讀寫而使用一個特權(quán)程序在每次打開它時改變一個設(shè)備的擁有權(quán)是不實際的.

迄今所顯示的代碼沒有實現(xiàn)任何的存取控制, 除了文件系統(tǒng)許可位. 如果系統(tǒng)調(diào)用 open 將請求遞交給驅(qū)動, open 就成功了. 我們現(xiàn)在介紹幾個新技術(shù)來實現(xiàn)一些額外的檢查.

每個在本節(jié)中展示的設(shè)備有和空的 scull 設(shè)備有相同的行為(即, 它實現(xiàn)一個持久的內(nèi)存區(qū))但是在存取控制方面和 scull 不同, 這個實現(xiàn)在 open 和 release 操作中.

6.6.1.?單 open 設(shè)備

提供存取控制的強力方式是只允許一個設(shè)備一次被一個進程打開(單次打開). 這個技術(shù)最好是避免因為它限制了用戶的靈活性. 一個用戶可能想運行不同的進程在一個設(shè)備上, 一個讀狀態(tài)信息而另一個寫數(shù)據(jù). 在某些情況下, 用戶通過一個外殼腳本運行幾個簡單的程序可做很多事情, 只要它們可并發(fā)存取設(shè)備. 換句話說, 實現(xiàn)一個單 open 行為實際是在創(chuàng)建策略, 這樣可能會介入你的用戶要做的范圍.

只允許單個進程打開設(shè)備有不期望的特性, 但是它也是一個設(shè)備驅(qū)動最簡單實現(xiàn)的存取控制, 因此它在這里被展示. 這個源碼是從一個稱為 scullsingle 的設(shè)備中提取的.

scullsingle 設(shè)備維護一個 atiomic_t 變量, 稱為 scull_s_available; 這個變量被初始化為值 1, 表示設(shè)備確實可用. open 調(diào)用遞減并測試 scull_s_available 并拒絕存取如果其他人已經(jīng)使設(shè)備打開.


static atomic_t scull_s_available = ATOMIC_INIT(1);
static int scull_s_open(struct inode *inode, struct file *filp)
{

        struct scull_dev *dev = &scull_s_device; /* device information */
        if (! atomic_dec_and_test (&scull_s_available))
        {
                atomic_inc(&scull_s_available);
                return -EBUSY; /* already open */
        }

        /* then, everything else is copied from the bare scull device */
        if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)

                scull_trim(dev);
        filp->private_data = dev;
        return 0; /* success */
}

release 調(diào)用, 另一方面, 標識設(shè)備為不再忙:


static int scull_s_release(struct inode *inode, struct file *filp)
{
        atomic_inc(&scull_s_available); /* release the device */
        return 0;
}

正常地, 我們建議你將 open 標志 scul_s_available 放在設(shè)備結(jié)構(gòu)中( scull_dev 這里), 因為, 從概念上, 它屬于這個設(shè)備. scull 驅(qū)動, 但是, 使用獨立的變量來保持這個標志, 因此它可使用和空 scull 設(shè)備同樣的設(shè)備結(jié)構(gòu)和方法, 并且最少的代碼復(fù)制.

6.6.2.?一次對一個用戶限制存取

單打開設(shè)備之外的下一步是使一個用戶在多個進程中打開一個設(shè)備, 但是一次只允許一個用戶打開設(shè)備. 這個解決方案使得容易測試設(shè)備, 因為用戶一次可從幾個進程讀寫, 但是假定這個用戶負責維護在多次存取中的數(shù)據(jù)完整性. 這通過在 open 方法中添加檢查來實現(xiàn); 這樣的檢查在通常的許可檢查后進行, 并且只能使存取更加嚴格, 比由擁有者和組許可位所指定的限制. 這是和 ttys 所用的存取策略是相同的, 但是它不依賴于外部的特權(quán)程序.

這些存取策略實現(xiàn)地有些比單打開策略要奇怪. 在這個情況下, 需要 2 項: 一個打開計數(shù)和設(shè)備擁有者 uid. 再一次, 給這個項的最好的地方是在設(shè)備結(jié)構(gòu)中; 我們的例子使用全局變量代替, 是因為之前為 scullsingle 所解釋的的原因. 這個設(shè)備的名子是 sculluid.

open 調(diào)用在第一次打開時同意了存取但是記住了設(shè)備擁有者. 這意味著一個用戶可打開設(shè)備多次, 因此允許協(xié)調(diào)多個進程對設(shè)備并發(fā)操作. 同時, 沒有其他用戶可打開它, 這樣避免了外部干擾. 因為這個函數(shù)版本幾乎和之前的一致, 這樣相關(guān)的部分在這里被復(fù)制:


spin_lock(&scull_u_lock);
if (scull_u_count &&
                (scull_u_owner != current->uid) && /* allow user */
                (scull_u_owner != current->euid) && /* allow whoever did su */
                !capable(CAP_DAC_OVERRIDE))
{ /* still allow root */
        spin_unlock(&scull_u_lock);
        return -EBUSY; /* -EPERM would confuse the user */
}

if (scull_u_count == 0)
        scull_u_owner = current->uid; /* grab it */

scull_u_count++;
spin_unlock(&scull_u_lock);

注意 sculluid 代碼有 2 個變量 ( scull_u_owner 和 scull_u_count)來控制對設(shè)備的存取, 并且這樣可被多個進程并發(fā)地存取. 為使這些變量安全, 我們使用一個自旋鎖控制對它們的存取( scull_u_lock ). 沒有這個鎖, 2 個(或多個)進程可同時測試 scull_u_count , 并且都可能認為它們擁有設(shè)備的擁有權(quán). 這里使用一個自旋鎖, 是因為這個鎖被持有極短的時間, 并且驅(qū)動在持有這個鎖時不做任何可睡眠的事情.

我們選擇返回 -EBUSY 而不是 -EPERM, 即便這個代碼在進行許可檢測, 為了給一個被拒絕存取的用戶指出正確的方向. 對于"許可拒絕"的反應(yīng)常常是檢查 /dev 文件的模式和擁有者, 而"設(shè)備忙"正確地建議用戶應(yīng)當尋找一個已經(jīng)在使用設(shè)備的進程.

這個代碼也檢查來看是否正在試圖打開的進程有能力來覆蓋文件存取許可; 如果是這樣, open 被允許即便打開進程不是設(shè)備的擁有者. CAP_DAC_OVERRIDE 能力在這個情況中適合這個任務(wù).

release 方法看來如下:


static int scull_u_release(struct inode *inode, struct file *filp)
{
        spin_lock(&scull_u_lock);
        scull_u_count--; /* nothing else */
        spin_unlock(&scull_u_lock);
        return 0;
}

再次, 我們在修改計數(shù)之前必須獲得鎖, 來確保我們沒有和另一個進程競爭.

6.6.3.?阻塞 open 作為對 EBUSY 的替代

當設(shè)備不可存取, 返回一個錯誤常常是最合理的方法, 但是有些情況用戶可能更愿意等待設(shè)備.

例如, 如果一個數(shù)據(jù)通訊通道既用于規(guī)律地預(yù)期地傳送報告(使用 crontab), 也用于根據(jù)用戶的需要偶爾地使用, 對于被安排的操作最好是稍微延遲, 而不是只是因為通道當前忙而失敗.

這是程序員在設(shè)計一個設(shè)備驅(qū)動時必須做的一個選擇之一, 并且正確的答案依賴正被解決的實際問題.

對 EBUSY 的替代, 如同你可能已經(jīng)想到的, 是實現(xiàn)阻塞 open. scullwuid 設(shè)備是一個在打開時等待設(shè)備而不是返回 -EBUSY 的 sculluid 版本. 它不同于 sculluid 只在下面的打開操作部分:


spin_lock(&scull_w_lock);
while (! scull_w_available())
{
        spin_unlock(&scull_w_lock);
        if (filp->f_flags & O_NONBLOCK)
                return -EAGAIN;
        if (wait_event_interruptible (scull_w_wait, scull_w_available()))
                return -ERESTARTSYS; /* tell the fs layer to handle it */
        spin_lock(&scull_w_lock);
}
if (scull_w_count == 0)
        scull_w_owner = current->uid; /* grab it */
scull_w_count++;
spin_unlock(&scull_w_lock);

這個實現(xiàn)再次基于一個等待隊列. 如果設(shè)備當前不可用, 試圖打開它的進程被放置到等待隊列直到擁有進程關(guān)閉設(shè)備.

release 方法, 接著, 負責喚醒任何掛起的進程:


static int scull_w_release(struct inode *inode, struct file *filp)
{

 int temp;
 spin_lock(&scull_w_lock);
 scull_w_count--;
 temp = scull_w_count;
 spin_unlock(&scull_w_lock); 
    if (temp == 0)
 wake_up_interruptible_sync(&scull_w_wait); /* awake other uid's */
 return 0;
}

這是一個例子, 這里調(diào)用 wake_up_interruptible_sync 是有意義的. 當我們做這個喚醒, 我們只是要返回到用戶空間, 這對于系統(tǒng)是一個自然的調(diào)度點. 當我們做這個喚醒時不是潛在地重新調(diào)度, 最好只是調(diào)用 "sync" 版本并且完成我們的工作.

阻塞式打開實現(xiàn)的問題是對于交互式用戶真的不好, 他們不得不猜想哪里出錯了. 交互式用戶常常調(diào)用標準命令, 例如 cp 和 tar, 并且不能增加 O_NONBLOCK 到 open 調(diào)用. 有些使用磁帶驅(qū)動器做備份的人可能喜歡有一個簡單的"設(shè)備或者資源忙"消息, 來替代被扔在一邊猜為什么今天的硬盤驅(qū)動器這么安靜, 此時 tar 應(yīng)當在掃描它.

這類的問題(需要一個不同的, 不兼容的策略對于同一個設(shè)備)最好通過為每個存取策略實現(xiàn)一個設(shè)備節(jié)點來實現(xiàn). 這個做法的一個例子可在 linux 磁帶驅(qū)動中找到, 它提供了多個設(shè)備文件給同一個設(shè)備. 例如, 不同的設(shè)備文件將使驅(qū)動器使用或者不用壓縮記錄, 或者自動回繞磁帶當設(shè)備被關(guān)閉時.

6.6.4.?在 open 時復(fù)制設(shè)備

管理存取控制的另一個技術(shù)是創(chuàng)建設(shè)備的不同的私有拷貝, 根據(jù)打開它的進程.

明顯地, 這只當設(shè)備沒有綁定到一個硬件實體時有可能; scull 是一個這樣的"軟件"設(shè)備的例子. /dev/tty 的內(nèi)部使用類似的技術(shù)來給它的進程一個不同的 /dev 入口點呈現(xiàn)的視圖. 當設(shè)備的拷貝被軟件驅(qū)動創(chuàng)建, 我們稱它們?yōu)樘摂M設(shè)備--就象虛擬控制臺使用一個物理 tty 設(shè)備.

結(jié)構(gòu)這類的存取控制很少需要, 這個實現(xiàn)可說明內(nèi)核代碼是多么容易改變應(yīng)用程序的對周圍世界的看法(即, 計算機).

/dev/scullpriv 設(shè)備節(jié)點在 scull 軟件包只實現(xiàn)虛擬設(shè)備. scullpriv 實現(xiàn)使用了進程的控制 tty 的設(shè)備號作為對存取虛擬設(shè)備的鑰匙. 但是, 你可以輕易地改變代碼來使用任何整數(shù)值作為鑰匙; 每個選擇都導(dǎo)致一個不同的策略. 例如, 使用 uid 導(dǎo)致一個不同地虛擬設(shè)備給每個用戶, 而使用一個 pid 鑰匙創(chuàng)建一個新設(shè)備為每個存取它的進程.

使用控制終端的決定打算用在易于使用 I/O 重定向測試設(shè)備: 設(shè)備被所有的在同一個虛擬終端運行的命令所共享, 并且保持獨立于在另一個終端上運行的命令所見到的.

open 方法看來象下面的代碼. 它必須尋找正確的虛擬設(shè)備并且可能創(chuàng)建一個. 這個函數(shù)的最后部分沒有展示, 因為它拷貝自空的 scull, 我們已經(jīng)見到過.


/* The clone-specific data structure includes a key field */
struct scull_listitem
{
        struct scull_dev device;
        dev_t key;
        struct list_head list;

};
/* The list of devices, and a lock to protect it */
static LIST_HEAD(scull_c_list);
static spinlock_t scull_c_lock = SPIN_LOCK_UNLOCKED;

/* Look for a device or create one if missing */
static struct scull_dev *scull_c_lookfor_device(dev_t key)
{

        struct scull_listitem *lptr;
        list_for_each_entry(lptr, &scull_c_list, list)
        {
                if (lptr->key == key)
                        return &(lptr->device);
        }

        /* not found */
        lptr = kmalloc(sizeof(struct scull_listitem), GFP_KERNEL);
        if (!lptr)
                return NULL;
        /* initialize the device */
        memset(lptr, 0, sizeof(struct scull_listitem));
        lptr->key = key;
        scull_trim(&(lptr->device)); /* initialize it */
        init_MUTEX(&(lptr->device.sem));

        /* place it in the list */
        list_add(&lptr->list, &scull_c_list);

        return &(lptr->device);
}

static int scull_c_open(struct inode *inode, struct file *filp)
{
        struct scull_dev *dev;

        dev_t key;
        if (!current->signal->tty)
        {
                PDEBUG("Process \"%s\" has no ctl tty\n", current->comm);
                return -EINVAL;

        }
        key = tty_devnum(current->signal->tty);

        /* look for a scullc device in the list */
        spin_lock(&scull_c_lock);
        dev = scull_c_lookfor_device(key);
        spin_unlock(&scull_c_lock);

        if (!dev)
                return -ENOMEM;

        /* then, everything else is copied from the bare scull device */

這個 release 方法沒有做特殊的事情. 它將在最后的關(guān)閉時正常地釋放設(shè)備, 但是我們不選擇來維護一個 open 計數(shù)而來簡化對驅(qū)動的測試. 如果設(shè)備在最后的關(guān)閉被釋放, 你將不能讀相同的數(shù)據(jù)在寫入設(shè)備之后, 除非一個后臺進程將保持它打開. 例子驅(qū)動采用了簡單的方法來保持數(shù)據(jù), 以便在下一次打開時, 你會發(fā)現(xiàn)它在那里. 設(shè)備在 scull_cleanup 被調(diào)用時釋放.

這個代碼使用通用的 linux 鏈表機制, 而不是從頭開始實現(xiàn)相同的功能. linux 鏈表在第 11 章中討論.

這里是 /dev/scullpriv 的 release 實現(xiàn), 它結(jié)束了對設(shè)備方法的討論.


static int scull_c_release(struct inode *inode, struct file *filp)
{
        /*
        *Nothing to do, because the device is persistent.
        *A `real' cloned device should be freed on last close */

        return 0;
}
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號