天天看點

Linux裝置驅動程式 三 字元裝置驅動

Linux裝置驅動程式 三 字元裝置驅動 筆記

第三章 字元驅動裝置

本章會編寫一個完整的字元裝置,字元裝置簡單,易于了解,

名字是scull:Simple Caracter Utility for Loading Localities,區域裝載的簡單字元工具,

scull是一個操作記憶體區域的字元裝置driver,這片記憶體區域就相當于一個裝置。

它不和硬體相關,隻是操作從核心配置設定的一些記憶體

1,scull的設計

第一步,定義driver為使用者提供的能力,也就是機制

我們的裝置是記憶體,他可以是順序或者随機存取裝置,可以是一個或者多個裝置,

我們實作了若幹裝置抽象,每個都有自己的特點,

由子產品實作的每種裝置稱為一種 類型 :

scull0 ~ scull3

這四個裝置分别由一個全局且持久的記憶體區域組成,

全局:若device被多次打開,則打開它的所有fd可共享這個device的資料

持久:如果devicce關閉再打開,data不會丢。

可以用常用指令通路和測試這個device,如cp,cat和shell的I/O重定向等

scullpipe0 ~ scullpipe3

這4個FIFO先入先出 裝置與管道類似。一個程序讀取另一個程序寫入的資料。

若多個程序讀取同一個device,他們會為資料發送競争。

scullpipe的内部實作說明在不借助中斷的情況下,如何實作阻塞式和非阻塞式 的 讀寫操作。

雖然實際的driver使用 硬體的中斷與他們的drivce保持同步,

但阻塞式和非阻塞式操作是重要的内容,有别于中斷處理

scullsingle

scullpriv

sculliud

scullwiud

這些裝置與scull0類似,但是何時允許open操作方面有些限制。

scullsingle一次隻允許一個程序使用這個driver,

scullpriv對每個虛拟控制台(或X終端會話)是私有的,這是因為每個控制台/終端上的程序将擷取不同的記憶體區。

sculluid和scullwuid可以被多次打開,但是每次隻能由一個使用者打開;

如果另一個使用者鎖定了device,sculluid傳回Device Busy error,

而scullwiud實作了阻塞式open。

這些scull裝置的變種混淆了機制和政策,但是值得了解,很多真正的裝置需要類似的管理方式

本章講scull0~scull3的内部結構,更複雜的在第六章将,scullpipe在 阻塞式I/O 講,其他在在 裝置檔案的通路控制 講。

2,主裝置号和次裝置号

對字元裝置的通路是通過檔案系統内的裝置名稱進行的。被稱為特殊檔案,裝置檔案,檔案系統樹的節點,

它們位于/dev/

ls -l時,字元裝置 在第一列用 c 識别。也可以看到主次裝置号,

主裝置号代表device對應的driver。一般一個主裝置号對應一個driver

次裝置号由核心使用,用于正确确定裝置檔案所指的裝置,

我們可以通過次裝置号獲得一個指向核心裝置的直接指針,也可以将次裝置号當做裝置本地數組的索引。

3,裝置編号的内部表達

dev_t(lnux/type.h)儲存裝置編号,包括主裝置号和次裝置号

dev_t是32bit,12bit代表主裝置号,其餘20bit代表次裝置号,

擷取方式:

MAJOR(dev_t dev);

MINOR(dev_t dev);

反向操作:

MKDEV(int major, int minor);

4,配置設定和釋放裝置編号

建立char deivce前, 先要獲得一個或者多個裝置編号。

通過register_chrdev_region,它在linux/fs.h 聲明:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

first是要配置設定的裝置編号的範圍的起始值,可以給0

count是請求的連續裝置編号的個數

name是裝置名稱,出現在/proc/devices和sysfs中

和多數核心函數一樣,成功return 0 ,

如果不知道裝置要使用的主裝置号,需要動态配置設定:

int alloc_chrdev_region(dev_t *dev, unsigned int firestminor, uint count, cahr *name);

dev用于輸出,調用成功後儲存配置設定的第一個編号。

firstminor可以給0,

釋放裝置編号:

void unregister_chrdev_region(dev_t first, unisnged int count);

一般在清除函數調用它

有了裝置編号,AP可以通路它,driver需要将裝置編号與内部函數連接配接起來,内部函數實作裝置的操作。

5,動态配置設定主裝置号

盡量用動态配置設定,配置設定了裝置号,就可以通過/proc/.devices/和/sys/中讀取得到

是以insmod可以替換為一個簡單的腳本

腳本調用insmod後讀取/proc/devices來獲得新配置設定的主裝置号,然後建立對應的裝置檔案。

可以利用awk從/proc/devices擷取資訊,在/dev/建立裝置檔案

130|console:/ # cat /proc/devices                                              
Character devices:
  1 mem
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
 10 misc
....
180 usb
188 ttyUSB
189 usb_device
216 rfcomm
227 s3gcard
244 roccat
245 hidraw
246 rtk_btusb
247 zxtz
248 bsg
249 watchdog
250 ptp
251 pps
252 media
253 rtc
254 tee

Block devices:
  1 ramdisk
259 blkext
  7 loop
....
134 sd
135 sd
179 mmc
254 device-mapper
           

腳本:

device="scull"

/sbin/insmod ./$module.ko $* || exit 1

#删除原有節點
rm -f /dev/${device}[0-3] 

major=$(awk "\$2= =\"$module\" {print \$1} /proc/devices)

mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
           

。。。。

6,一些重要的資料結構

三個重要的核心資料結構:

file_oerations,file,inode

在編寫真正的drvier前,要對他們有個基本的認識

7,file_operations結構體

上面我們保留了裝置編号,但是沒有與driver建立連接配接。

file_operations就是用來建立連接配接的,定義在linux/fd.h,包含了一組函數指針。

每個打開的檔案(内部用一個file表示)和一組函數關聯(通過包含指向file_operations的f_op字段)

這些操作主要用來實作系統調用如open,read等,

可以認為檔案是一個對象,操作它的函數是方法,用oop的術語:對象聲明的動作将作用于其本身。

這是linux核心看到的面向對象程式設計的第一個例子,後面更多。

按照慣例,file_operations結構或者指向這類結構的指針稱為fops,

這個結構中的每個字段必須指向驅動程式中實作特定操作的函數,不支援就置null

通讀file_operations方法的清單時,會看到許多參數包含__user字元傳,它表明指針是一個使用者空間的位址,不能被直接引用。

struct module *owner

unsigned int (*poll) (strcut file *, struct poll_table_struct *);

poll方法是poll,epoll,select三個系統調用的後端實作,他們可以用來查詢某個或者多個fd上讀取或寫入是否會被阻塞。

poll傳回一個位掩碼,指出非阻塞的讀取或者寫入是否可能,并且也會向核心提供将調用程序置于休眠狀态直到I/O變為可能時的資訊。

如果poll置null,device被認為可讀可寫不會阻塞,、

int (*mmap)(struct file *, struct vm_area_struct *);

mmap用于請求将裝置記憶體映射到程序位址空間,

scull device driver程式實作的隻是最重要的裝置方法,file_operations結構init為:

struct file_operations scull_fops = {
    .owner = THIS_MODULE,
    ...
    .read = scull_read,
    .ioctl = scull_ioctl,
    .open = scull_open,
    .realse = scull_release,
}
           

這個聲明采用标準c的标記化結構初始化文法,

這種文法值得采用,因為它讓driver在結構的定義發生變化時更有可移植性,使代碼更緊湊和易讀。

标記化的init方法允許對結構成員重新排列,

某些場合,把頻繁 被通路的成員放在相同的硬體緩存行,可大大提高性能

8,file結構體

struct file是driver的第二個重要的資料結構, 定義在linux/fs.h

file結構與UMD的FILE沒有任何關聯。FILE是C庫定義的,不會出現在核心的代碼。

file結構代表一個打開的檔案,不僅driver,每個打開的檔案在KMD都對應一個file結構,

核心在open時建立它,并傳遞給在這個檔案上進行操作的所有函數,

核心源碼中,指向struct file的指針叫filp(檔案指針),

struct file的重要成員:

mode_t f_mode,檔案模式

struct file_operations *f_op; //與檔案相關的操作

核心在open時指派這個指針,以後需要處理這些操作時讀這個指針,

filp->f_op的值不會為友善引用而儲存起來,。。。。

這裡提到了,這種替換檔案操作的能力在OOP裡叫 方法重載

void *private_data;

private_data是跨系統調用時儲存狀态資訊的非常有用的資源,

記得要在核心銷毀file前在release方法裡釋放它的記憶體

9,inode結構體

核心用inode結構體在内部表示檔案,它與file不同,file是打開的檔案描述符。

有用字段:

dev_t i_rdev;

包含了真正的裝置編号

struct cdev *i_cdev;

cdev表示字元裝置的核心的内部結構。當inode指向一個字元裝置檔案時,

這個字段包含了指向struct cdev的指針

10,字元裝置的注冊

核心内部用struct cdev表示字元裝置,

linux/cdev.h

//配置設定和init cdev有兩種方法,
//如果要在運作時擷取一個獨立的cdev結構:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
//可以把cdev結構嵌入自己的裝置特定結構中,scull就是這樣做的,
//這時,必須用下面的代碼去init
void cdev_init(struct cdev *cdev, struct file_operations *fops);

//告訴核心改結構的資訊
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

//Scull的裝置注冊
//scull通過struct scull_dev的結構表示每一個裝置,結構定義如下:
struct scull_dev {
    struct scull_qset *data; //指向第一個量子集的指針,
    int quantum;  //目前量子的大小
    int qset; //目前數組的大小
    unisgned long size; //資料總量
    unsigned int access_key; 
    struct semaphore sem; //互斥信号量
    struct cdev cdev;  //字元裝置結構
}

//現在我們集中注意力在cdev,即核心和裝置之間的接口struct cdev.
//struct cdev必須如上述被init并添加到系統,
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor+index);
    
    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULKE;
    dev->cdev.ops = &scull_fops;
    err = cdev_add(&dev->cdev, devno, 1);
    if(err)
        printk(KERN_NOTICE"Error %d\n",err);
}
因為cdev被嵌入了struct scull_dev中,是以必須調用cdev_init執行該結構的初始化,
           

11,早期的方法

int register_chrdev(unsigned int major, const char *name, struct file_opertions *fops);

它會為給定的主裝置号注冊0~255次裝置号,并為每個裝置建立一個對應的預設cdev結構。

name是driver的名稱,出現在/proc/devices中,

s3g的init方法:

new_kernel/linux/s3.c
static int __init s3g_init(void)
{
    //#define S3G_DEV_NAME "s3gcard"  @s3g_ioctl.h
    //#define S3G_PROC_NAME "driver/s3g"
    ret = register_chrdev(S3G_MAJOR, S3G_DEV_NAME, &s3g_fops);
    
    //struct class *s3g_class
    s3g_class =  class_create(THIS_MODULE, S3G_DEV_NAME);
    
    //初始化s3g_card[0]全局變量,通過s3g_card_init(s3g, NULL)
    //裡面也會建立proc節點
    ret = s3g_register_driver();
}

//linux/s3g_drvier.c
int s3g_card_init(s3g_card_t *s3g, void *pdev)
{
    proc_create_data(S3G_PROC_NAME, 0, NULL, &s3g_proc_fops, s3g);
}

static const struct file)operations s3g_proc_fops = 
{
    .owner = THIS_MODULE,
    .read = s3g_proc_read,
    .write = s3g_proc_write,
};
           

12,open和release

看看file_operations的的字段如何使用,

open需要完成的工作:

1,檢查裝置特定錯誤,如硬體沒有就緒

2,若device首次打開,init它

3,若有必要,更新f_op指針

4,配置設定并填寫置于flip->private_data的資料結構

s3.c有一個file_operations,linux_fb為什麼沒有?是因為使用了linux核心的接口嗎?

s3.h include了所有需要的核心的頭檔案

//new_kernel/linux/s3.h
#ifdef __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/time.h>
#include <linux/interrupt.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/shmem_fs.h>
#include <linux/writeback.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/file.h>
#include <linux/leds.h>
#include <linux/platform_device.h>
#include <linux/console.h>
#include <linux/version.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/mm.h>
#include <linux/pagemap.h>
#include <linux/vmalloc.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/anon_inodes.h>
#include <linux/list.h>
#include <linux/input.h>
#include <linux/rwsem.h>
#include <linux/seq_file.h>
#include <linux/debugfs.h>
#include <linux/gpio.h>
#include <linux/regulator/consumer.h>
           

open的原型:

int (*open)(struct inode *inode, struct file *flip)

inode在i_cdev字段包含了我們需要的資訊,就是我們之前設定的cdev結構,/

然而我們通常不需要cdev,而是需要包含cdev的scull_dev結構,

C語言的一些技巧可以完成這類轉換,但不該濫用,因為難以了解,幸好核心黑客已經實作了這個技巧,

//linux/kernel.h

contaner_of(pointer, container_type, contaner_field);

它需要一個contaner_field字段的指針,它包含在container_type類型的結構中,

傳回包含該字段的結構指針,

在scull_open中,這個宏用來找到适當的裝置結構,

struct scull_dev *dev;

dev = container_of(inode->i_cdev, struct scull_dev, cdev);

參數:cdev的指針,包含cdev字段的結構體,cdev字段。

找到scull_dev結構後,儲存到file結構的private_data字段,友善以後對該指針的通路。

略微簡化的scull_open代碼:

int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev;
    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev;
    
    //trim to 0.....if open was write-onlyu
    if( flip->f_flags == O_WRONLY) {
        scull_trim(dev);
    }
    return 0; //success
}
           

這段代碼很小,因為沒有針對某個特定device的處理,

scull裝置是全局且持久的,是以不用特别處理。

而且不維護scull的打開計數,值維護子產品的使用計數,也就沒有“首次打開init device”的動作,

對device唯一的操作是,如果以寫方式打開,長度就截斷為0,。

因為當用更短的檔案覆寫一個scull裝置時,裝置資料區應該縮小,

就像寫檔案打開時,長度截斷為0,

如何打開s3gcard driver并使用它?

打開用open,其餘操作全在ioctl

//libkeinterface/s3g_keinterface.c
int s3gOpenMinor(int minor)
{
    char path[64];
    sprintf(path, "%s%s%d", S3G_DIR_NAME, S3G_DEV_NAME, minor);
    if((fd = open(path, O_RDWR, 0)) >= 0)
    {
        return fd;
    }
    return -errno;
}


//gralloc/gralloc_s3g.c
__attribute_((constructor)) void gralloc_s3g_init()
{
    gralloc_s3g_t *s3g = &HAL_MODULE_INFO_SYM;
    //all gralloc device share one s3g device
    fd = s3gOpenMinor(0);
    status = s3gCreateDdevice(fd, &create);
    //儲存fd和device
    s3g->fd = fd;
    s3g->device = create.device;
}

//libkeinterface/s3g_keinterface.c
int s3gCreateDevice(int fd, s3g_create_device_t* create_device)
{
    if(ioctl(fd, S3G_IOCTL_CREATE_DEVICE, create_device))
    {
        return -errno;
    }
    return 0;
}
           

13,release方法

任務:

1,釋放由open配置設定的,儲存在flip->private_data的所有内容,

2,在最後一次操作時關閉裝置

如果關閉device的次數比打開的多怎麼辦?

因為dup和fork系統調用都是不調用open,就建立以打開檔案的副本。但是每個副本在程式終止時都要關閉。

如,多數程式從來不打開他們的stdin裝置,但是都在終止時關閉,

driver如何知道何時真正關閉?

回答:

不是每個close系統調用都引起release的調用,真正釋放裝置資料結構的close才做。

核心對每個file結構維護其被使用次數的計數器。

fork和dup,都不會建立新的資料結構,隻有open才會建立新的資料結構,fork和dup隻是增加已有結構的計數。

隻有file結構的計數歸0時,close系統調用才會執行release方法,

這樣就保證了一次open對應一次release

在程序退出時,核心在内部用close系統調用自動關閉所有相關檔案,

14,scull的記憶體使用,

引入核心 記憶體管理的核心函數,在linux/slab.h

void *kmalloc(size_t size, int flags); void kree(void *ptr);

kmalloc配置設定size位元組的記憶體,傳回其指針,flags一般是GFP_KERNEL,

對于配置設定大記憶體區,kmalloc不是最好的方法,第八章講,

配置設定整個頁面更有效

為測試記憶體短缺,可以讓scull吃光記憶體,

cp /dev/zero /dev/scull0,可以用光RAM

也可以用dd工具選擇複制多少資料到scull裝置

scull中,每個裝置都是一個指針連結清單,每個指針指向一個scull_qset結構,

用來一個有1000個指針的數組,每個指針指向一個4000位元組的區域。

。。。。介紹了scull_qset資料結構相關

14,read和write

他們的任務是copy data到UMD,反過來就是從UMD copy data。是以原型相似:

ssize_T read(struct file *filp, char __user *buff, size_t count, loff_t *offp);

ssize_t write(struct file *filp, const char __user *buffer, size_t count, loff_t *offp);

filp是檔案指針,count是請求傳輸的資料長度,buffer是UMD的緩沖區,

offp是指向long offset type對象的指針,它指明使用者在檔案中做存取操作的位置。

buff是UMD的指針,核心不能直接引用其中的内容,原因:

1,UMD的指針在KMD可能是無效的,該位址可能根本無法被映射到KMD,

2,即使指針在KMD代表相同的東西,但UMD的記憶體是分頁的,而在系統調用被調用時,涉及的記憶體可能根本不在RAM中,

對UMD記憶體的直接飲用會導緻頁錯誤,oops會導緻調用這個系統調用的程序死亡。

3,不安全,如果driver盲目引用UMD指針,UMD就可以随意通路和覆寫系統記憶體

這種通路由UMD專用函數完成,在asm/uaccess.h定義,六章ioctl介紹。

對于UMD和KMD的資料拷貝,使用:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long count); unsigned long copy_frome_user(void *to, const void __user *from, unsigned long count);

它的行為像memcpy,但是要小心,

被尋址的UMD的page可能不在記憶體,這是虛拟記憶體子系統會讓程序進入休眠狀态,直到page在期望的位置,

如page必須從swap空間取回時。

這樣的結果是,通路UMD的函數必須是可重入的,必須能和其他driver函數并發執行,第五章讨論

他們得作用不限于copy data,還檢查了UMD的指針是否有效,

無效UMD point在第六章讨論。

下面介紹了read和write的代碼