字符设备驱动(1):Linux字符设备驱动结构

简介: 字符设备驱动(1):Linux字符设备驱动结构

前言

在整个Linux设备驱动的学习中,字符设备驱动较为基础。本章将讲解Linux字符设备驱动程序的结构,并解释其主要组成部分的编程方法。

1 Linux字符设备驱动结构

1-cdev结构体

1struct cdev {
2  struct kobject kobj;             /* 内嵌的kobject对象 */
3  struct module *owner;            /* 所属模块*/
4  struct file_operations *ops;     /* 文件操作结构体*/
5  struct list_head list;
6  dev_t dev;                       /* 设备号*/
7  unsigned int count;
8 };

cdev结构体的dev_t成员定义了设备号,为32位,其中12位为主设备号20位为次设备号。使用下列宏可以从dev_t获得主设备号和次设备号:

MAJOR(dev_t dev)
MINOR(dev_t dev)

而使用下列宏则可以通过主设备号和次设备号生成dev_t:

MKDEV(int major, int minor)

cdev结构体的另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。

Linux内核提供了一组函数以用于操作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 *);

cdev_init()函数用于初始化cdev的成员并建立cdev和file_operations之间的连接,其源代码如代码清单6.2所示。

1void cdev_init(struct cdev *cdev, struct file_operations *fops)
2{
3    memset(cdev, 0, sizeof *cdev);
4    INIT_LIST_HEAD(&cdev->list);
5    kobject_init(&cdev->kobj, &ktype_cdev_default);
6    cdev->ops = fops; /* 将传入的文件操作结构体指针赋值给cdev的ops*/
7 }

cdev_alloc()函数用于动态申请一个cdev内存,其源代码如代码清单6.3所示。

1struct cdev *cdev_alloc(void)
2{
3        struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
4        if (p) {
5            INIT_LIST_HEAD(&p->list);
6            kobject_init(&p->kobj, &ktype_cdev_dynamic);
7        }
8        return p;
9 }

cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。

对cdev_add()的调用通常发生在字符设备驱动模块加载函数中而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。

2-分配和释放设备号

在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型为:

int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中。

alloc_chrdev_region()相比于register_chrdev_region()的优点在于它会自动避开设备号重复的冲突。

相应地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型为:

void unregister_chrdev_region(dev_t from, unsigned count);

3-file_operations结构体

file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被内核调用。file_operations结构体目前已经比较庞大,它的定义如代码清单6.4所示。

1struct file_operations {
 2  struct module *owner;
 3  loff_t (*llseek) (struct file *, loff_t, int);
 4  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
 5  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
 6  ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
 7  ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
 8  int (*iterate) (struct file *, struct dir_context *);
 9  unsigned int (*poll) (struct file *, struct poll_table_struct *);
10  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
11  long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
12  int (*mmap) (struct file *, struct vm_area_struct *);
13  int (*open) (struct inode *, struct file *);
14  int (*flush) (struct file *, fl_owner_t id);
15  int (*release) (struct inode *, struct file *);
16  int (*fsync) (struct file *, loff_t, loff_t, int datasync);
17  int (*aio_fsync) (struct kiocb *, int datasync);
18  int (*fasync) (int, struct file *, int);
19  int (*lock) (struct file *, int, struct file_lock *);
20  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
21  unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
         unsigned long, unsigned long);
22  int (*check_flags)(int);
23  int (*flock) (struct file *, int, struct file_lock *);
24  ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,  size_t, unsigned int);
25  ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,  size_t, unsigned int);
26  int (*setlease)(struct file *, long, struct file_lock **);
27  long (*fallocate)(struct file *file, int mode, loff_t offset,
28               loff_t len);
29  int (*show_fdinfo)(struct seq_file *m, struct file *f);
30};

下面我们对file_operations结构体中的主要成员进行分析。

  • llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
  • read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。它与用户空间应用程序中的ssize_t read(int fd,voidbuf,size_t count)和size_t fread(voidptr,size_t size,size_t nmemb,FILE*stream)对应。
  • write()函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。它与用户空间应用程序中的ssize_t write(int fd,const voidbuf,size_t count)和size_t fwrite(const voidptr,size_t size,size_t nmemb,FILE*stream)对应。
  • read()和write()如果返回0,则暗示end-of-file(EOF)。
  • unlocked_ioctl()提供设备相关控制命令的实现(既不是读操作,也不是写操作),当调用成功时,返回给调用程序一个非负值。它与用户空间应用程序调用的int fcntl(int fd,int cmd,…/arg/)和int ioctl(int d,int request,…)对应。
  • mmap()函数将设备内存映射到进程的虚拟地址空间中,如果设备驱动未实现此函数,用户进行mmap()系统调用时将获得-ENODEV返回值。这个函数对于帧缓冲等设备特别有意义,帧缓冲被映射到用户空间后,应用程序可以直接访问它而无须在内核和应用间进行内存复制。它与用户空间应用程序中的voidmmap(voidaddr,size_t length,int prot,int flags,int fd,off_t offset)函数对应。
  • 当用户空间调用Linux API函数open()打开设备文件时,设备驱动的open()函数最终被调用。驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。与open()函数对应的是release()函数。
  • poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程的阻塞。
  • aio_read()和aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符执行SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy等系统调用进行读写。

4-Linux字符设备驱动的组成

在Linux中,字符设备驱动由如下几个部分组成。

1.字符设备驱动模块加载与卸载函数

在字符设备驱动

  • 模块加载函数中应该实现设备号的申请和cdev的注册
  • 而在卸载函数中应实现设备号的释放和cdev的注销。

Linux内核的编码习惯是为设备定义一个设备相关的结构体,该结构体包含设备所涉及的cdev、私有数据及锁等信息

常见的设备结构体、模块加载和卸载函数形式如代码清单6.5所示。

1/* 设备结构体
 2struct xxx_dev_t {
 3    struct cdev cdev;
 4  ...
 5} xxx_dev;
 6/* 设备驱动模块加载函数
 7static int _ _init xxx_init(void)
 8{
 9  ...
10  cdev_init(&xxx_dev.cdev, &xxx_fops);         /* 初始化cdev */
11  xxx_dev.cdev.owner = THIS_MODULE;
12  /* 获取字符设备号*/
13  if (xxx_major) {
14      register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
15  } else {
16      alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
17  }
18
19  ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1);  /* 注册设备*/
20  ...
21}
22/* 设备驱动模块卸载函数*/
23static void _ _exit xxx_exit(void)
24{
25   unregister_chrdev_region(xxx_dev_no, 1);      /* 释放占用的设备号*/
26   cdev_del(&xxx_dev.cdev);                      /* 注销设备*/
27  ...
28 }

2.字符设备驱动的file_operations结构体中的成员函数

file_operations结构体中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。

大多数字符设备驱动会实现read()、write()和ioctl()函数,常见的字符设备驱动的这3个函数的形式如代码清单6.6所示。

1 /* 读设备*/
 2 ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,
 3    loff_t*f_pos)
 4 {
 5    ...
 6    copy_to_user(buf, ..., ...);
 7    ...
 8 }
 9 /* 写设备*/
10 ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count,
11    loff_t *f_pos)
12 {
13    ...
14    copy_from_user(..., buf, ...);
15    ...
16 }
17 /* ioctl函数 */
18 long xxx_ioctl(struct file *filp, unsigned int cmd,
19    unsigned long arg)
20 {
21    ...
22    switch (cmd) {
23    case XXX_CMD1:
24         ...
25         break;
26    case XXX_CMD2:
27         ...
28         break;
29    default:
30         /* 不能支持的命令 */
31         return  - ENOTTY;
32    }
33    return 0;
34 }

设备驱动的读函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不宜直接读写,count是要读的字节数,f_pos是读的位置相对于文件开头的偏移。

设备驱动的写函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不宜直接读写,count是要写的字节数,f_pos是写的位置相对于文件开头的偏移。

由于用户空间不能直接访问内核空间的内存,因此借助了函数copy_from_user()完成用户空间缓冲区到内核空间的复制,以及copy_to_user()完成内核空间到用户空间缓冲区的复制,见代码第6行和第14行。

完成内核空间和用户空间内存复制的copy_from_user()和copy_to_user()的原型分别为:

unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);
unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);

上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。如果复制失败,则返回负值。

如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user(),如:

int val;                         /* 内核空间整型变量
...
get_user(val, (int *) arg);      /* 用户→内核,arg是用户空间的地址 */
...
put_user(val, (int *) arg);      /* 内核→用户,arg是用户空间的地址 */

读和写函数中的_user是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释的功能。这个宏定义为:

#ifdef _ _CHECKER_ _
# define _ _user      _ _attribute_ _((noderef, address_space(1)))
#else
# define _ _user
#endif

内核空间虽然可以访问用户空间的缓冲区,但是在访问之前,一般需要先检查其合法性,通过access_ok(type,addr,size)进行判断,以确定传入的缓冲区的确属于用户空间,例如:

static ssize_t read_port(struct file *file, char __user *buf,
                         size_t count, loff_t *ppos)
{
        unsigned long i = *ppos;
        char __user *tmp = buf;
        if (!access_ok(VERIFY_WRITE, buf, count))
                return -EFAULT;
        while (count-- > 0 && i < 65536) {
                if (__put_user(inb(i), tmp) < 0)
                        return -EFAULT;
                i++;
                tmp++;
        }
        *ppos = i;
        return tmp-buf;
}

上述代码中引用的__put_user()与前文讲解的put_user()的区别在于前者不进行类似access_ok()的检查,而后者会进行这一检查。

在本例中,不使用put_user()而使用__put_user()的原因是在__put_user()调用之前,已经手动检查了用户空间缓冲区(buf指向的大小为count的内存)的合法性。get_user()和__get_user()的区别也相似。

特别要提醒读者注意的是:在内核空间与用户空间的界面处,内核检查用户空间缓冲区的合法性显得尤其必要,Linux内核的许多安全漏洞都是因为遗忘了这一检查造成的,非法侵入者可以伪造一片内核空间的缓冲区地址传入系统调用的接口,让内核对这个evil指针指向的内核空间填充数据。有兴趣的读者可以从http://www.cvedetails.com/网站查阅Linux CVE(Common Vulnerabilities and Exposures)列表。

其实copy_from_user()、copy_to_user()内部也进行了这样的检查:

static inline unsigned long __must_check copy_from_user(void *to, const void __user
    *from, unsigned long n)
{
        if (access_ok(VERIFY_READ, from, n))
                n = __copy_from_user(to, from, n);
        else /* security hole - plug it */
                memset(to, 0, n);
        return n;
}
static inline unsigned long __must_check copy_to_user(void __user *to, const void
    *from, unsigned long n)
{
        if (access_ok(VERIFY_WRITE, to, n))
                n = __copy_to_user(to, from, n);
        return n;
}

I/O控制函数的cmd参数为事先定义的I/O控制命令,而arg为对应于该命令的参数。例如对于串行设备,如果SET_BAUDRATE是一道设置波特率的命令,那后面的arg就应该是波特率值。

在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员,如代码清单6.7所示。

1struct file_operations xxx_fops = {
2     .owner = THIS_MODULE,
3     .read = xxx_read,
4     .write = xxx_write,
5     .unlocked_ioctl= xxx_ioctl,
6     ...
7 };

上述xxx_fops在代码清单6.5第10行的cdev_init(&xxx_dev.cdev,&xxx_fops)的语句中建立与cdev的连接。

图6.1所示为字符设备驱动的结构字符设备驱动字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。

内容参考:Linux设备驱动开发详解

目录
相关文章
|
1月前
|
Linux 调度
Linux系统结构
内核是操作系统的核心组件,负责管理系统资源和硬件设备。它提供了硬件抽象层,使得应用程序不必直接操作硬件。内核的主要功能包括: 进程管理:创建、调度和终止进程。 内存管理:分配和回收内存,提供虚拟内存功能。 设备管理:控制硬件设备的访问。 文件系统管理:管理文件和目录结构。 系统调用接口:提供系统调用,使应用程序能够与操作系统交互。
41 8
|
4月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
54 6
|
4月前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
58 5
|
4月前
|
存储 缓存 Unix
Linux 设备驱动程序(三)(上)
Linux 设备驱动程序(三)
49 3
|
4月前
|
缓存 安全 Linux
Linux 设备驱动程序(一)((下)
Linux 设备驱动程序(一)
44 3
|
4月前
|
Linux
Linux 设备驱动程序(四)
Linux 设备驱动程序(四)
31 1
|
4月前
|
存储 数据采集 缓存
Linux 设备驱动程序(三)(中)
Linux 设备驱动程序(三)
49 1
|
4月前
|
存储 前端开发 大数据
Linux 设备驱动程序(二)(中)
Linux 设备驱动程序(二)
34 1
|
4月前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
46 1
|
3月前
|
Linux API
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】