Linux字符设备驱动学习

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自旋锁
    信号量可能允许多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥
    信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量。

    信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋
    锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程
    一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我
    们就应该选择信号量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值