您的位置:首页 > 运维架构 > Linux

Linux字符设备驱动学习

2010-04-13 12:41 302 查看
1、设备号的作用

   主设备号用来标识与设备文件相连的驱动程序。次设备号被驱动用来辩别操作的是那个设备

   主设备号用来反映设备类型

   次设备号用来区分通类型的设备

2、内核中如何描述设备号?

   dev_t //其实质为unsigned int 32为整数,其中高12位为主设备号,低20位为次设备号

  

   如何从dev_t中分解出主设备号?

   MAJOR(dev_t dev)

   如何从dev_t中分解出设备号?

   MINOR(dev_t dev)

   linux内核如何给设备分配主设备号?

   可以采用静态申请,动态申请两种方法

3、静态申请

   方法:1、根据Documentation/devices.txt,确定一个没有使用的主设备号

         2、使用register_chrdev_region函数注册设备号

   优点:简单

   缺点:一旦驱动被广泛使用,这个随机选定的设备号可能会导致设备号冲突,

         而使驱动程序无法注册

   int register_chrdev_region(dev_t from, unsigned count, const char *name)

   功能:

        注册从from开始的count个设备号(主设备号不变,次设备号增加,如果次设备号益处,主设备号加1)

   参数:

        from: 要注册的第一个设备号

        count:要注册的设备号个数

        name: 设备名(体现在/proc/devices)

4、动态分配

   方法:    使用alloc_chrdev_region分配设备号

   优点:    简单,易于驱动推广

   缺点:    无法在安装之前创建设备文件(因为安装前还没分配到主设备号)

   解决办法:安装驱动后,从/proc/devices中查询设备号

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

   功能:

        动态申请count个设备号,第一个设备号的次设备号为baseminor。

   参数:

        dev:      分配到的设备号

        baseminor:起始次设备号

        count:    要注册的设备号个数

        name:     设备名(体现在/proc/devices)

5、注销设备号

   不论使用何种方法分配设备号,都应该在不再使用它们的时候释放这些设备号。

    void unregister_chrdev_region(dev_t from, unsigned count)

    功能:释放从from开始的count个设备号

6、创建设备文件

   6.1、手动创建:

   使用mknod: mknod filename type major monor

   filename: 设备文件名

   type:     设备文件类型

   major:    主设备号

   minor:    次设备号

   例:mknod serial0 c 100 0

   6.2、动态创建:

7、重要结构

   在Linux字符设备驱动程序设计中,有3种非常重要的数据结构:

   struct file         struct inode        struct file_operations

   7.1、struct file

        代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的

        struct file。它由内核再打开文件时创建,在文件关闭后释放。

        重要成员:

            loff_t f_pos  //文件读写位置

            struct file_operations *f_op

   7.2、struct inode

        用来记录文件的物理上的信息。因此,它和代表打开文件的file结构是不同的。

        一个文件可以对应多个file结构,但只有一个inode结构。

        重要成员:

            dev_t i_rdev:设备号

   7.3、struct file_operations

        一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,

        这些函数实现一个特别的操作,对于不支持的操作保留为NULL

        struct file_operations mem_fops = {

       .owner = THIS_MODULE,

       .llseek = mem_seek,

       .read = mem_read,

       .write = mem_write,

       .ioctl = mem_ioctl,

       .open = mem_open,

       .release = mem_release,

       };

8、设备注册

   在Linux2.6内核中,字符设备使用struct cdev来描述。

   字符设备的注册可分为如下3个步骤:

   1、分配cdev

   2、初始化cdev

   3、添加cdev

   8.1、struct cdev的初始化使用cdev_init函数来完成。

        void cdev_init(struct cdev *cdev, const struct file_operations *fops)

        参数:

        cdev:初始化的cdev结构

        fops:设备对应的操作函数集

   8.2、struct cdev的注册使用cdev_add函数来完成。

        int cdev_add(struct cdev *p, dev_t dev, unsigned count)

        参数:

        p:待添加到内核的字符设备结构

        dev:设备号

        count:添加的设备个数

9、设备操作

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

   在设备文件上的第一个操作,并不要求驱动程序一定要

   void (*release)(struct inode *, struct file *)

   当设备文件被关闭时调用这个操作。与open相仿,release也可以没有。

   ssize_t (*read)(struct file *, char __user *, size_t, loff_t *)

   从设备中读取数据

   ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *)

   向设备发送数据。

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

   对应select系统调用

   int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long)

   控制设备

10、读和写

    读和写方法都完成类似的工作:从设备中读取数据到用户空间;将数据传递给驱动程序。

    它们的原型也相当相似: 

    ssize_t xxx_read(struct file *file, char __user *buff, size_t count, loff_t *offp);

    ssize_t xxx_read(struct file *file, char __user *buff, size_t count, loff_t *offp);

    对于2个方法,file是文件指针,count是请求传输的数据量。buff参数指向数据缓存。

    最后,offp指出文件当前的访问位置。

    10.2、read和write方法的buff参数是用户空间指针。因此,它不能被内核代码直接引用,理由如下:

       用户空间指针在内核空间时可能根本是无效的---没有那个地址的映射。

       内核提供了专门的函数用于访问用户空间的指针

       int copy_from_user(void *to, const void __user *from, int n)

       int copy_to_suser(void __user *to, const void *from, int n)

11、设备注销

    字符设备的注销使用cdev_del函数来完成。

    int cdev_del(struct cdev *p)

    参数:p要注销的字符设备结构

12、并发与竞态

    并发:多个执行单元同时被执行。

    竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量等)的访问导致的竞争状态。

    处理并发的常用技术是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作

    共享资源。在Linux内核中主要通过semaphore机制和spin_lock机制实现。

    12.1、信号量

    信号量的实现也是与体系结构相关的,定义在<asm/semaphore.h>中,

    struct semaphore 类型用来表示信号量。

    定义信号量

        struct semaphore sem;

    初始化信号量

        void sema_init(struct semaphore *sem, int val)

    该函数用于初始化设置信号量的初值,它设置信号量sem的值为val。

        void init_MUTEX(struct semaphore *sem)

    该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。

        void init_MUTEX_LOCKED(struct semaphore *sem)

    该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始

    就处在已锁状态。

    定义和初始化的工作可由如下宏一步完成:

        DECLARE_MUTEX(name)

    定义一个信号量name,并初始化它的值为1。

        DECLARE_MUTEX_LOCKED(name)

    定义一个信号量name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。

    获取信号量

        void down(struct semaphore *sem)

    获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文使用该函数。

    该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,

    直到别的任务释放该信号量才能继续运行。

        void down_interruptible(struct semaphore *sem)

    获取信号量sem。如果信号量不可用,进程将被置为TASK_INTERRUPTIBLE类型的睡眠装填。

    该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,

    如果被信号打断,返回-EINTER。

        void down_killable(struct semaphore *sem)

    获取信号量sem。如果信号量不可用,进程将被设置为TASK_LILLABLE类型的睡眠状态。

    注:down()函数现已不建议继续使用。建议使用down_killable()或down_interruptible()

        void up(struct semaphore *sem)

    该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,

    因此唤醒这些等待者。

13、自旋锁

    自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试

    图获得一个已经被只有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里看看

    是否该自旋锁的保持者已经释放了锁,“自旋”就是这个意思。

    信号量是一种睡眠锁,自旋锁是忙等。

        spin_lock_init(x)

    试图获取自旋锁lock,如果能立即获得锁,并返回真,否则立即返回假。它不会一直等待被释放。

        spin_unlock(lock)

    释放自旋锁lock,它与spin_trylock或者spin_lock配对使用。

14、信号量PK自旋锁

    信号量可能允许多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥

    信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量。

    信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋

    锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程

    一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我

    们就应该选择信号量。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息