天天看點

Linux 字元裝置驅動

目錄

  • 字元裝置
  • Linux字元裝置驅動結構
    • cdev結構體
    • 配置設定、釋放裝置号
      • 配置設定裝置号
      • 釋放裝置号
    • file_operations結構體
  • 字元裝置驅動的組成
    • 字元裝置驅動子產品的加載、解除安裝函數
    • 字元裝置驅動的file_operations結構體的成員函數
      • copy_to_user和copy_from_user
      • put_user和get_user
      • I/O控制函數unlocked_ioctl
      • 字元裝置驅動檔案操作file_operations
  • 參考

這部分主要講Linux字元裝置驅動程式的結構,解釋主要組成部分的程式設計方法。

字元裝置

字元裝置:指隻能一個byte一個byte讀寫的裝置,不能随機讀寫資料,要按先後順序。字元裝置是面向流的裝置,常見字元裝置有滑鼠、鍵盤、序列槽、終端、LED燈。

塊裝置:指可以從裝置的任意位置讀取一定長度資料的裝置。常見塊裝置有磁盤、硬碟、U盤、SD卡等。

每個字元裝置或塊裝置,都在/dev目錄下有一個對應的裝置檔案。Linux APP可以通過這些裝置檔案(又稱裝置節點),來使用驅動程式操作字元裝置和塊裝置。

字元裝置、字元裝置驅動與使用者空間通路該裝置的程式三者之間的關系:

Linux 字元裝置驅動

from Linux 字元裝置驅動結構(一)—— cdev 結構體、裝置号相關知識解析 | CSDN

Linux核心中,

  • 使用cdev結構體描述字元裝置;
  • 通過成員dev_t定義裝置号(主、次裝置号),确定字元裝置的唯一性;
  • 通過成員file_operations定義字元裝置驅動,為VFS(虛拟檔案系統)提供接口函數,如open/close/read/write等。

Linux字元裝置驅動中,

  • 子產品加載函數通過register_chrdev_region()或alloc_chrdev_region(),來靜态或動态擷取裝置号;
  • 通過cdev_init()建立cdev與file_opreations之間的連接配接,通過cdev_add()向系統添加一個cdev以完成注冊;
  • 子產品解除安裝函數通過cdev_del()來登出cdev,通過unregister_chrdev_region()來釋放裝置号。

TIPS: register_chrdev 與 register_chrdev_region, alloc_chrdev_region有何差別?

register_chrdev 裝置注冊 + 裝置号申請。register_chrdev_region和alloc_chrdev_region 裝置号申請,裝置注冊由cdev_init + cdev_add完成。

register_chrdev() 支援一次注冊一個裝置,而且需要傳入參數file_operations。預設寫死注冊的裝置号範圍0~255。釋放字元裝置時,使用unregister_chrdev()。但不必使用cdev_xxx系列操作。

register_chrdev_region() 支援一次注冊多個裝置号,不需要傳入參數file_operations,在cdev_init()中綁定cdev與file_operations。釋放裝置号時,使用unregister_chrdev_region。register_chrdev_region需要搭配cdev_xxx系列操作使用。

alloc_chrdev_region() 與register_chrdev_region()的差別在于前者申請的裝置号由系統決定,後者由調用者指定。

APP中通路裝置驅動程式,

  • 通過Linux系統調用,如open/close/read/write,調用file_operations中定義的接口函數。

Linux字元裝置驅動結構

cdev結構體

Linux核心中,使用cdev結構體描述一個字元裝置。cdev定義:

#include <linux/cdev.h>

struct cdev {
    struct kobject kobj;           /* 内嵌的kobject對象 */
    struct module *owner;          /* 所屬子產品 */
    struct file_operations *ops;   /* 檔案操作結構體 */
    struct list_head list;
    dev_t dev;                     /* 裝置号 */
    unsigned int count;            /* 該裝置關聯的裝置編号的數量 */
};
           

cdev結構體的dev_t成員定義裝置号(32bit),其中高12bit為主裝置号,低20bit為次裝置号。

如何擷取主次裝置号,或dev_t?

  • 從dev_t獲得主裝置号和次裝置号
MAJOR(dev_t dev); // 主裝置号
MINOR(dev_t dev); // 次裝置号
           
  • 通過主裝置号、次裝置号生成dev_t
MKDEV(int major, int minor); // 生成dev_t, 包含主次裝置号資訊
           

這幾個宏定義如下:

#include <linux/kdev_t.h>

#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)

#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS)) // 高12bit為主裝置号
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))  // 低20bit為次裝置号
#define MKDEV(ma,mi)  (((ma) << MINORBITS) | (mi))
           

核心提供一組函數用于操作cdev結構體:

void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int  cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
           

1)cdev_init 初始化cdev成員,最重要的是建立cdev和file_operations之間的連接配接

源碼:

/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev); /* 将整個結構體清零 */
    INIT_LIST_HEAD(&cdev->list);   /* 初始化list成員, 指向自身 */
    kobject_init(&cdev->kobj, &ktype_cdev_default); /* 初始化kobj成員 */
    cdev->ops = fops; /* 建立cdev和file_operations之間的連接配接 */
}
           

2)cdev_alloc 動态申請一個cdev記憶體

源碼:

/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void)
{
    struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); /* 動态申請一個cdev記憶體, GFP_KERNEL: 無記憶體可用時可休眠 */
    if (p) {
        INIT_LIST_HEAD(&p->list);  /* 初始化list成員, 指向自身 */
        kobject_init(&p->kobj, &ktype_cdev_dynamic);  /* 初始化kobj成員 */
    }
    return p;
}
           

上面兩個初始化函數,為何都沒看到owner、dev、count 這3個成員的初始化?

對于owner成員,struct module類型對象,是核心對于一個子產品的抽象。該成員在字元裝置中可以展現該裝置隸屬于哪個子產品,在驅動程式的編寫中一般由使用者顯式初始化.owner = THIS_MODULE

對于dev和count成員,在cdev_add中才會指派。

3)cdev_add 向核心添加一個cdev,完成字元裝置的注冊

這裡需要提供參數dev(裝置号)和count(該裝置關聯的裝置編号的數量),直接指派給cdev結構的dev和count成員。

/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
*         device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately.  A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    int error;

    p->dev = dev;
    p->count = count;

    error = kobj_map(cdev_map, dev, count, NULL,
             exact_match, exact_lock, p); /* 将cdev放入cdev_map中 */
    if (error)
        return error;

    kobject_get(p->kobj.parent); /* 增加引用計數 */

    return 0;
}
           

4)cdev_del 從核心删除一個cdev

/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p)
{
    cdev_unmap(p->dev, p->count);      /* 将dev從cdev_map中擦除 */
    kobject_put(&p->kobj);             /* 減少引用計數 */
}

static void cdev_unmap(dev_t dev, unsigned count)
{
    kobj_unmap(cdev_map, dev, count); /* 将dev從cdev_map中擦除 */
}
           

配置設定、釋放裝置号

配置設定裝置号

調用cdev_add()向系統注冊字元裝置前,應先申請裝置号。配置設定裝置号有2種方法:

1)靜态申請:register_chrdev_region

register_chrdev_region() 用于已知起始裝置号的情況,向系統靜态申請裝置号(範圍)。

要申請的裝置号範圍:[from, from + count)。

有些裝置号已被Linux核心開發者配置設定掉了,具體配置設定内容可檢視Documentation/devices.txt。

/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
*        the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
    struct char_device_struct *cd;
    dev_t to = from + count;
    dev_t n, next;

    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0);
        if (next > to)
            next = to;
        cd = __register_chrdev_region(MAJOR(n), MINOR(n),
                   next - n, name);
        if (IS_ERR(cd))
            goto fail;
    }
    return 0;
fail: /* 出錯復原 */
    to = n;
    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0);
        kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
    }
    return PTR_ERR(cd);
}
           

2)動态申請:alloc_chrdev_region

alloc_chrdev_region() 用于裝置号未知,向系統動态申請未被占用的裝置号的情況。

得到的裝置号會放入第一個參數dev中。alloc_chrdev_region相比register_chrdev_region,優點:alloc_chrdev_region會自動避開裝置号重複的沖突。

/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers.  The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev.  Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
{
    struct char_device_struct *cd;             /* 字元裝置結構指針 */
    cd = __register_chrdev_region(0, baseminor, count, name); /* 注冊單個指定主裝置号、次裝置号 */
    if (IS_ERR(cd))
        return PTR_ERR(cd);
    *dev = MKDEV(cd->major, cd->baseminor);
    return 0;
}
           

檢查:注冊裝置成功後,會在/proc/devices 添加字元裝置名稱。

是以,可以利用insmod指令加載裝置驅動後,觀察/proc/devices值,判斷是否注冊了裝置。

# cat /proc/devices
           

釋放裝置号

在調用cdev_del()從系統登出字元裝置後,unregister_chrdev_region()應該被調用以釋放原先申請的裝置号。

從系統反注冊裝置号,範圍:[from, from + count)

/**
* unregister_chrdev_region() - unregister a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from.  The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count)
{
    dev_t to = from + count;
    dev_t n, next;

    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0); /* 下一個裝置号dev_t */
        if (next > to)
            next = to;
        kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); /* 反注冊單個裝置号, 并釋放空間 */
    }
}
           

file_operations結構體

file_operations 是裝置驅動程式與APP互動的接口,其成員函數是字元裝置驅動程式設計的主體,實際會在APP調用open/write/read/close等系統調用時被核心調用。

file_operations結構體定義:

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
            loff_t, size_t, unsigned int);
    int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
            u64);
    ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
            u64);
};
           

主要成員:

  • llseek() 用來修改一個檔案的目前讀寫位置,并将新位置傳回,出錯時,函數傳回一個負值。
  • read() 用來從裝置中讀取資料,成功時函數傳回讀取的位元組數,出錯時傳回一個負值。對應使用者空間read(2)。
  • write() 向裝置發送資料,成功時函數傳回寫入的位元組數。如果未被實作,當使用者進行write()系統調用時,将得到-EINVAL傳回值。對應使用者空間write(2)。

read和write傳回0,暗示end-of-line(EOF)。

  • unlocked_ioctl() 提供裝置相關控制指令的實作(不是讀,也不是寫),成功時傳回一個非負值。對應使用者空間fcntl(2)應。
  • mmap() 将裝置記憶體映射到程序的虛拟位址空間,如果裝置驅動未實作該函數,使用者調用mmap()系統調用時傳回-ENODEV。對應使用者空間mmap(2)。與mmap對應的是unmap。
  • open() 打開裝置,用于初始化裝置狀态。使用者空間調用open(2)時,裝置驅動的open()被調用。驅動程式可以不實作該函數,裝置打開操作永遠成功。與open對應的是release。
  • release() 釋放裝置資源。如果open()中有申請系統資源,則可以在release()中釋放。對應使用者空間close(2)。
  • poll() 用于詢問裝置是否可以被非阻塞地立即讀寫。當詢問的條件未觸發時,使用者空間進行select()和poll()系統調用将引起程序阻塞。
  • aio_read()/aio_write() 分别對與檔案描述符對應的裝置進行異步讀、寫操作。裝置實作這2個函數後,使用者空間可以對該裝置檔案描述符執行SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy等系統調用進行讀寫。

字元裝置驅動的組成

Linux中,字元裝置驅動組成:字元裝置驅動子產品加載、解除安裝函數,字元裝置驅動的file_operations結構體的成員函數。

字元裝置驅動子產品的加載、解除安裝函數

加載函數應該實作:1)裝置号的申請;2)cdev的注冊。

解除安裝函數應該實作:1)裝置号的釋放;2)cdev的登出。

典型的裝置結構體、子產品加載函數、解除安裝函數代碼形式:

/* 裝置結構體
struct xxx_dev_t = {
    struct cdev cdev;
    ...
} xxx_dev;
 */
/* 裝置驅動子產品加載函數 */
static init __init xxx_init(void)
{
    ...
    cdev_init(&xxx_dev.cdev, &xxx_fops);          /* 初始化cdev */
    xxx_dev.cdev.owner = THIS_MODULE;
    /* 獲得字元裝置号 */
    if (xxx_major) {
        register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
    } else {
        alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
    }

    ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注冊裝置 */
    ...
}

/* 裝置驅動子產品解除安裝函數 */
static void __exit xxx_exit(void)
{
    unregister_chrdev_region(xxx_dev_no, 1);     /* 釋放占用的裝置号 */
    cdev_del(&xxx_dev.cdev);                     /* 登出裝置 */
}
           

字元裝置驅動的file_operations結構體的成員函數

file_operations的成員函數是字元裝置驅動跟核心虛拟檔案系統的接口,是使用者空間對Linux進行系統調用最終的落實者。大多數字元裝置驅動會實作read()/write/ioctl()。

典型字元裝置驅動代碼形式:

/* 讀裝置
 * filp: 檔案結構指針
 * buf: 使用者空間記憶體位址, 在核心空間不能直接讀寫
 * count: 要讀的位元組數
 * f_pos: 讀的位置相對于檔案開頭的偏移
 */
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    ...
    copy_to_user(buf, ..., ...); /* 将資料從核心空間拷貝到使用者空間 */
    ...
}

/* 寫裝置 */
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    ...
    copy_from_user(.... buf, ...); /* 将資料從使用者空間拷貝到核心空間 */
    ...
}

/* ioctl函數 */
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    ...
    switch(cmd) {
    case XXX_CMD1:
        ...
        break;
    case XXX_CMD2:
        ...
        break;
    default:
        /* 不支援的指令 */
        return -ENOTTY;
    }
    return 0;
}
           

copy_to_user和copy_from_user

注:使用者空間不能直接通路核心空間的記憶體,是以要借助copy_to_user()将資料從核心空間拷貝到使用者空間;

同樣地,核心空間不能直接通路使用者空間的記憶體,是以借助copy_from_user()将資料從使用者空間拷貝到核心空間。

#include <linux/uaccess.h>

/* 使用者 -> 核心 */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
/* 核心 -> 使用者 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
注:函數傳回不能被複制的位元組數。如果完全複制成功,傳回0;如果失敗,傳回負值。
           

其源碼如下:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
    if (likely(access_ok(VERIFY_READ, from, n))) /* 檢查位址的合法性, from起始位址, 長度n */
        n = __copy_from_user(to, from, n);       /* 資料拷貝, 但不做位址合法性檢查 */
    else
        memset(to, 0, n);
    return n;
}

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
    if (likely(access_ok(VERIFY_WRITE, to, n))) /* 檢查位址的合法性, to起始位址, 長度n */
        n = __copy_to_user(to, from, n);        /* 資料拷貝, 但不做位址合法性檢查 */
    return n;
}
           

likely:是宏定義,常用于編譯器優化,告訴編譯器分支大機率會發生。

access_ok(type, addr, size):核心空間可以通路使用者空間的緩沖區,但通路之前需要用access_ok檢查其合法性,以确定傳入的緩沖區位址的确術語使用者空間。

如果要複制的記憶體是簡單類型,如char、int、long等,則可以使用簡單的put_user()和get_user()。

int val;                    /* 核心空間變量 */
...
get_user(val, (int* ) arg); /* 使用者 -> 核心, arg 是使用者空間位址 */
...
put_user(val, (int* ) arg); /* 核心 -> 使用者, arg 是使用者空間位址 */
           

copy_from_user函數中的__user宏是什麼?

該宏表明背後的指針指向使用者空間,實際上更多地充當了代碼注釋的功能。

#ifdef __CHECKER__
# define __user __attribute__((noderef, address_space(1)))
#else
# define __user
#endif
           

put_user和get_user

put_user(), get_user() 也有另外一個版本:__put_user(), __get_user()。差別在于__put_user()不用access_ok()檢查位址的合法性,而put_user()會。通常,在調用__put_user()之前,會手動檢查使用者空間緩沖區。

get_user()和__get_user() 關系類似。

I/O控制函數unlocked_ioctl

I/O控制函數的cmd參數為事先定義的I/O控制指令,arg為對應于指令的參數。例如,對于串行裝置,如果SET_BAUDRATE是設定波特率的指令,那arg就應該是波特率值。

字元裝置驅動檔案操作file_operations

字元裝置驅動檔案操作,通過定義file_operations執行個體,并将具體裝置驅動函數指派給file_operations成員來完成。

struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .read = xxx_read,
    .write = xxx_write,
    .unlocked_ioctl = xxx_ioctl,
};
           

通過子產品加載函數中調用cdev_init(&xxx_dev.cdev, &xxx_fops) 為cdev和fops建立連接配接。

參考

[1]宋寶華. Linux裝置驅動開發詳解[M]. 人民郵電出版社, 2010.

[2] https://blog.csdn.net/zqixiao_09/article/details/50839042

繼續閱讀