Linux设备驱动程序简介

On September 28, 2010, in linux, linux内核, by sponge

因为最近项目关系,需要动态二进制翻译一个驱动,所以看了下驱动相关知识,并在这里做一个简单普及性的介绍。
本文主要参考两本书:《linux设备驱动程序》《Linux那些事儿之我是USB》
.
首先,我们都知道,Linux分为用户态和内核态,一般应用程序是在用户态执行,他们通过一系列的系统调用同内核态进行交互。那么驱动程序扮演一个什么样的角色呢?
.
驱动程序是内核与硬件的接口,他把系统调用映射到具体设备对于实际硬件的特定操作上,关系如下图所示:

.
通过这种方法,应用程序就可以像操作普通文件一样操作硬件设备,用户程序只需要关心这个抽象出来的文件,而一切同硬件打交道的工作都交给了驱动程序。比如我们要向屏幕输出一串字符串,我们只需要把字符串写到显卡所抽象出来的文件里,而真正把字符串传到显卡里的工作就是由驱动程序来完成的。
.
在Linux下,驱动程序是内核的一部分,运行在内核态下,你可以将驱动静态的和内核编译在一起,这样的缺点是内核会比较大,而且如果驱动出错,会导致整个系统崩溃;也可以以module的方式编译,在需要的时候动态的载入。如果你编译过内核,应该记得在make menuconfig中,选项前面是可以选择和的,就分别表示"编译到内核中"和"编译成模块"。
.
下面介绍下模块,一个简单的“helloworld module”如下所示:

/*  hello-1.c - The simplest kernel module.
 */
#include
  /* Needed by all modules */
#include
  /* Needed for KERN_ALERT */
 
int init_module(void)
{
   printk("<1>Hello world 1.\n");
 
   // A non 0 return means init_module failed; module can't be loaded.
   return 0;
}
 
void cleanup_module(void)
{
  printk(KERN_ALERT "Goodbye world 1.\n");
}

其中,init_module函数是加载模块时会被调用的,一般作一些初始化的工作;cleanup_module函数是卸载模块时会被调用的,做一些清理的工作。因为模块是运行在内核态的,你自然不能使用库函数,因此要打印信息,需要使用printk函数而不是printf函数。另外,你可以使用任意函数名(只要同内核函数名不冲突)来替换init_module和cleanup_module这两个函数名,但必须使用module_init(初始化函数名),module_exit(卸载时函数名)这两个宏来声明一下,也就是说,下面这个模块和上面的模块是等价的:

/*  hello-1.c - The simplest kernel module.
 */
#include
  /* Needed by all modules */
#include
  /* Needed for KERN_ALERT */
 
int helloworld(void)
{
   printk("<1>Hello world 1.\n");
 
   // A non 0 return means init_module failed; module can't be loaded.
   return 0;
}
 
void goodbyeworld(void)
{
  printk(KERN_ALERT "Goodbye world 1.\n");
}
module_init(helloworld);
module_exit(goodbyeworld);

.
编译和加载一个模块也很容易:

从上面的图可以看到,我们通过make编译生成一个模块文件".ko"之后,使用"insmod"命令来加载模块,那么“insmod”具体做了什么呢?
.
下面让我们来介绍下insmod这个工具:
insmod是linux下加载模块的工具,路径一般是/sbin/insmod,当你调用这个工具后,它的工作基本如下:

  • 在用户空间打开待安装的module
  • 调用query_module()系统调用询问无法落实的符号在内核或其他模块中的地址
  • 链接操作,落实模块中的符号引用
  • 调用create_module()系统调用在内核中创建module数据结构,并申请所需的内核空间
  • 调用init_module()系统调用将链接好的module映像装入内核空间,然后调用模块中的init_module()函数(注意:这里面的两个init_module函数不一样,一个是系统调用,一个是你写在模块里面的函数
  • .
    内核导出的符号清单可以由下面的命令来查看:

    more /proc/k[all]syms
    c0400000 T _text
    c0400000 T startup_32
    c040007b t default_entry
    c04000d0 T startup_32_smp
    c0400152 t checkCPUtype
    c04001d3 t is486
    c04001da t is386
    c0400247 t check_x87
    c040027a t setup_idt
    …
    c06adb25 T printk
    …

    .
    从已加载模块中卸载模块使用的是“rmmod”,"rmmod"所做工作如下:

  • 调用delete_module()系统调用释放模块的module结构,同时释放模块所占的内核空间
  • 调用模块中的cleanup_module()的函数
  • .

    那么,一般情况下,驱动程序会在init_module()和cleanu_module()函数中做些什么呢?


  • init_module(): 向内核登记本模块中一些包含着函数指针的数据结构(file_operations)
  • cleanup_module(): 向内核撤销本模块提供的数据结构的登记,使内核在模块拆卸后不至于再企图访问这些数据结构
  • .

    一个字符型驱动的注册函数如下:

    #include
     
    #inlcude
     
    int register_chrdev(unsigned int major, const char *name,
                    struct file_operations *fops);
  • major: 设备的主设备号,若为空则系统动态分配
  • name: 设备名
  • fops: 函数指针结构,各个调用的入口
  • 操作成功,设备名出现在/proc/devices文件
  • 至于怎么创建一个有设备名和设备号的文件,可以通过man mknod获取信息。
    .
    file_operantions的结构如下:

    Linux-2.6.27.25
    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 *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    ……
    }

    .
    此外,初始化部分还负责为设备驱动申请系统资源,如:内存,时钟,中断,I/O端口等。
    .
    最后,给出一张加载和卸载模块的结构图:

    anyShare分享到:
              

    1 Response » to “Linux设备驱动程序简介”

    Leave a Reply

    Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).