Driver:模块参数、系统调用、字符设备驱动框架

来源:互联网 发布:windows 10企业版激活 编辑:程序博客网 时间:2024/06/05 02:36
1、模块参数
    ./a.out xxx yyy zzz
    int main (int argc, char** argv) {...}// 用户空间
    insmod xxx.ko mmm nnn
        1)定义全局变量
        2)将该全局变量声明为模块参数
            'module_param (name, type, perm);
           // 指定模块参数,用于加载模块或模块加载以后传递参数给模块
                @name:变量的名称
                @type:变量的数据类型,支持类型如下:
                      bool
                      int
                      short
                      long
                      charp // char*
                perm:模块参数的访问权限,类似文件权限
                      rwxrwxrwx 0777 user group other
            'module_param_array (name, type, nump, perm);
            // 将一个数组声明为模块参数
                @type:数组成员成名为模块参数
                @nump:数组元素个数指针(有效成员的地址)
/** 代码演示 - moduleparam.c **/#include <linux/init.h>#include <linux/module.h>MODULE_LICENSE ("GPL");int irq = 0;char* pstr = "jiangyuan";int fish[10];int nr_fish = 10; module_param (irq, int, 0600);module_param (pstr, charp, 0); module_param_array (fish, int, &nr_fish, 0644);int __init moduleparam_init (void) {    int i = 0;    printk ("irq = %d\n", irq);    printk ("pstr = %s\n", pstr);    for (i = 0; i < nr_fish; i++) {        printk ("fish[%d] = %d\n", i, fish[i]);    }       return 0;}void __exit moduleparam_exit (void) {    int i = 0;    printk ("irq = %d\n", irq);    printk ("pstr = %s\n", pstr);    for (i = 0; i < nr_fish; i++) {        printk ("fish[%d] = %d\n", i, fish[i]);    }       }module_init (moduleparam_init);module_exit (moduleparam_exit);
/** Makefile **/obj-m   += moduleparam.oall:        make -C /home/tarena/jy/driver/kernel M=$(PWD) modules        cp *.ko ../../rootfs -vclean:        make -C /home/tarena/jy/driver/kernel M=$(PWD) clean

验证:
#:'insmod moduleparam.ko
    [  305.153000] irq = 0
    [  305.153000] pstr = jiangyuan
    [  305.153000] fish[0] = 0
    [  305.154000] fish[1] = 0
    [  305.155000] fish[2] = 0
    [  305.156000] fish[3] = 0
    [  305.157000] fish[4] = 0
    [  305.158000] fish[5] = 0
    [  305.159000] fish[6] = 0
    [  305.160000] fish[7] = 0
    [  305.161000] fish[8] = 0
    [  305.162000] fish[9] = 0
#:'rmmod moduleparam
#:'insmod moduleparam.ko irq=10 pstr="hello,world" fish=1,2,3,4,5
    [  448.455000] irq = 10
    [  448.455000] pstr = hello,world
    [  448.456000] fish[0] = 1
    [  448.457000] fish[1] = 2
    [  448.459000] fish[2] = 3
    [  448.460000] fish[3] = 4
    [  448.461000] fish[4] = 5

#:' ls /sys/module/moduleparam/parameters/ -l
total 0
-rw-r--r--    1 root     0             4096 Jan  1 00:58 fish
-rw-------    1 root     0             4096 Jan  1 00:58 irq
#:'cat /sys/module/moduleparam/parameters/irq
10
#:'cat /sys/module/moduleparam/parameters/fish
1,2,3,4,5
#:'echo 111 >/sys/module/moduleparam/parameters/irq
#:'echo 222,333,444,555 >/sys/module/moduleparam/parameters/fish
#:'cat /sys/module/moduleparam/parameters/irq
111
#:'cat /sys/module/moduleparam/parameters/fish
222,333,444,555
#:'rmmod moduleparam
    [  531.125000] irq = 111
    [  531.125000] pstr = hello,world
    [  531.125000] fish[0] = 222
    [  531.126000] fish[1] = 333
    [  531.127000] fish[2] = 444
    [  531.128000] fish[3] = 555

【总结】'以上参数用在驱动代码调试阶段。'比如:特殊功能寄存器的赋值...

    /sys/目录类似于/proc目录:基于内存的虚拟文件系统。
        在2.6内核中引入了/sys/目录
        导出驱动内核模型

2、系统调用
    【注意点】谈谈对系统调用的理解?

2.1 系统调用的作用
    是用户态切换到内核态的一种方式。
    unix的C编程程序大多数情况下运行于用户态,当产生系统调用就进入内核态。
2.2 系统调用时如何实现的
    1.进程先将系统调用号填充寄存器;
    2.调用一个特殊的指令swi;
    3.让用户进程跳转到内核事先定义好的一个位置;
    4.内核位置是ENTRY(vector_swi);//entry-common.s
    5.检查系统调用号,这个号告诉内核请求哪种服务;
    6.查看系统调用表(sys_call_table) 找到所调用的内核函数入口地址;
    7.调用该函数,执行,执行完毕返回到用户进程

    eg:
        open ("a.txt", ...)
            适当的值:如果是open,值就是5
                arch/arm/include/asm/unistd.h  // 定义内核源码 - 共365个
            寄存器:对于ARM处理器 r7
            特殊的指令:
                arm ---> swi / svc 
                x86 ---> int
                (调用后产生一个中断异常,硬件自动做4件事...)
            固定的位置:
                entry-common.S (e:\porting\kernel\arch\arm\kernel)
                    ENTRY (vector_swi):
                        sys_call_table[R7]
                    sys_call_table 存在于calls.s
                        sys_open

2.3 添加一个新的系统调用
    1)在内核中增加一个新的函数
        $:'cd kernel/
        $:'vi arch/arm/kernel/sys_arm.c
            135 asmlinkage int sys_add (int x, int y) 
            136 {
            137     printk ("enter %s\n", __func__);
            138     return x + y;
            139 }
    2)更新unistd.h
        $:'vi arch/arm/include/asm/unistd.h
            407 #define __NR_add                    (__NR_SYSCALL_BASE+378)
    3)更新系统调用表 sys_call_table
        $:'vi arch/arm/kernel/calls.S
            390         CALL(sys_add)  /* 378 */
    4)重新编译内核
        $:'make uImage
    5)开发板使用新的内核
        $:'cp arch/arm/boot/uImage /tftpboot/
        #:'tftp 48000000 uImage
        #:'bootm 48000000
    6)编写测试程序,调用编号为378的内核函数
        open ("a.txt", O_RDONLY);
        syscall (5, "a.txt", O_RDONLY);
/** 代码演示 - test.c **/#include <stdio.h>#include <unistd.h>#include <sys/syscall.h>int add (int x, int y) {return syscall (378, x, y);}int main (void){int res = 0;res = add (100, 200);printf ("res = %d\n", res);return 0;}
$:'arm-cortex_a9-linux-gnueabi-gcc test.c -o test
$:'cp test ../../rootfs/
    // nfs网络加载的开发板根文件系统在rootfs里面
$:'cp /opt/arm-<tab>/arm-<tab>/sysroot/lib/libgcc_s.so* ../../rootfs/lib/ -d
#:'./test
    [   13.627000] enter sys_add
    res = 300

<tips>
$:'find ./ -name "libgcc*"
按文件名查找,结果给出找到的所有路径


3、字符设备驱动框架
    字符设备:'读写顺序固定,读写过程中不涉及缓存。'如键盘... (较多)
    块设备:读写顺序不固定,读写过程中有缓存。如磁盘/硬盘...(基本已标准化)
    网络设备:读写顺序固定,读写过程中有缓存。如网卡...(可能涉及一点移植)

    linux内核主要是使用C语言实现的,但是其中运用了大量的面向对象的编程思想。
    使用'结构体'来实现面向对象编程。

    实现一个字符设备驱动,实则就是实例化一个 struct cdev:
    1) 定义struct cdev类型变量;
    2) 初始化变量;
    3) 注册变量。

    
struct cdev {
    dev_t dev; // 设备号(3.1)
    const struct file_operations *ops; // 操作函数集合 (3.2)
    ...
};

3.1 【设备号】
    dev_t 
    ← typedef __kernel_dev_t 
    ← typedef __u32 __kernel_dev_t 
    ← #define __u32 unsigned int

    设备号(32bit) = 主设备号(高12bit) + 次设备号(低20bit)

    主设备号:用来区分不同类型的设备。
    次设备号:用来区分同类设备中的不同个体。
    #:'ls /dev/ttySAC* -l

3.1.1 静态注册设备号
      选出一个内核中未被使用的主设备号,为我所用。
      主设备的取值范围: 0~255
      如何查看内核哪些主设备号没有被占用?
        1) #:' cat /proc/devices
            显示信息为:[主设备号] [使用设备的模块]
            比如 200 没被占用,就可用。
        2) $:' cat Documentation/devices.txt | less

    【静态注册设备号的方法】
    'register_chrdev_region'
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    功能:注册一个范围的设备号
    参数:
        @from:要注册的连续多个设备号中的第一个,必须是主设备号
                0x12300000
        @count:要注册的设备号个数
               3 要注册的设备号就是 0x12300000
                                   0x12300001
                                   0x12300002
        @name:名称
    返回值: 0 - 成功,负数 - 失败。

    'unregister_chrdev_region'
    void unregister_chrdev_region(dev_t from, unsigned count)
    功能:注销设备号
    参数:同register_chrdev_region
    返回值:无

/** 代码演示 - char_drv.c **/#include <linux/init.h>#include <linux/module.h>#include <linux/fs.h>     // 正常情况下找到使用函数的地方,头文件信息可全拷MODULE_LICENSE ("GPL");dev_t dev;                // 存储设备号unsigned int major = 200; // 主设备号unsigned int minor = 0;   // 次设备号int __init char_drv_init (void){    /* 注册设备号 */    // dev = major << 20 | minor; // 设备号    // #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))    dev = MKDEV (major, minor);    register_chrdev_region (dev, 1, "jiangyuan-1610");    return 0;}void __exit char_drv_exit (void){    /* 注销设备号 */    unregister_chrdev_region (dev, 1); }module_init (char_drv_init);module_exit (char_drv_exit);/** Makefile **/obj-m+= char_drv.oall:make -C /home/tarena/jy/driver/kernel M=$(PWD) modulescp *.ko ../../rootfs -vclean:make -C /home/tarena/jy/driver/kernel M=$(PWD) clean
验证:
    #:' insmod char_drv.ko
    #:'cat /proc/devices
        200 jiangyuan-1610


3.1.2 动态注册设备号
      由内核帮我们挑一个未被使用的设备号,和我们自己选中的次设备号拼成设备号。

    【动态注册设备号的方法】
    'alloc_chrdev_region'
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
    功能:动态分配一个设备号范围
    参数:
        @dev:传出参数 → 用于返回注册的多个设备号中的第一个。
        @baseminor:自己规定的起始次设备号
        @count:连续注册的设备号个数
        @name:名称
    返回值: 0 - 成功,负数 - 失败。
    
    假如:
    baseminor = 100
    内核给分配的主设备号假如是 200
    count = 3
    相当于向内核注册以下设备号:
        200 << 20 | 100
        200 << 20 | 100 + 1
        200 << 20 | 100 + 2

    'unregister_chrdev_region'
    void unregister_chrdev_region(dev_t from, unsigned count)
    功能:注销设备号 ('静态和动态均是其注销')
    参数:同register_chrdev_region
    返回值:无

/** 代码演示 - char_drv.c **/#include <linux/init.h>#include <linux/module.h>#include <linux/fs.h>MODULE_LICENSE ("GPL");dev_t dev;                // 存储设备号unsigned int major = 0;   // 主设备号unsigned int minor = 100;   // 次设备号int __init char_drv_init (void){    if (major) {        /* 静态注册 */        dev = MKDEV (major, minor);        register_chrdev_region (dev, 1, "jiangyuan-1610");    }       else {        /* 动态注册 */        alloc_chrdev_region (&dev, minor, 1, "jiangyuan-alloc-1610");    }       return 0;}void __exit char_drv_exit (void){    /* 注销设备号 */    unregister_chrdev_region (dev, 1); }module_init (char_drv_init);module_exit (char_drv_exit);
验证:
    #:' insmod char_drv.ko
    #:'cat /proc/devices
    #:'rmmod char_drv.ko
    #:' cat /proc/devices

3.2 【操作函数集合】
    完成一个字符设备驱动就是实例化一个 cdev 结构体。
   实例化一个cdev过程,主要工作就是实现其对应的操作函数的集合。

    struct file_operations {
        read ()
        write ()
        open ()
        ...
    };
    
    以上数据结构是所有字符设备驱动要实现的函数的合集,再去完成某个具体硬件字符设备驱动时,只需要实现其中的一部分函数就可以了。(根据具体的情况)

3.3 内核中提供操作cdev的API
    struct cdev{
        dev
        file_operation
        ...
    };

    'cdev_init'// cdev的初始化函数
    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    功能:初始化一个设备结构体
    参数:
        @cdev:要初始化的设备结构体变量地址
        @fops:要操作的设备的结构体变量地址
    返回值:无

    'cdev_add'  // 注册cdev
    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    功能:添加一个字符设备到系统
    参数:
        @p:要注册到系统中去的cdev地址
        @dev:对应的设备号
        @count:连续注册的个数,一个就取1
    返回值: 0 - 成功,负数 - 失败。

    'cdev_del'  // 注销cdev
    void cdev_del(struct cdev *p)
    功能:删除一个cdev字符设备
    参数:
        @p:要从系统注销的cdev地址
    返回值:无

/** 代码演示 -  **/
(暂略)


实验步骤:
    1) 安装查看模块
       #:' insmod char_drv.ko
       #:'cat /proc/devices
       244 ...
    2) 创建设备文件
        #:'mknod /dev/leds c 244 100
    3) 写一个test.c
        $:' vi test.c
        $:' arm-cortex_a9-linux-gnueabi-gcc test.c -o test
        $:' cp test ../../rootfs/
        $:'./test
            enter led_open success...
            open /dev/leds successed...
            now is closing device!enter led_release success...
实验总结:
没有增加新的系统调用,但也调用到了open和release函数...

设备文件时用户态程序调用硬件驱动的媒介(c major minor)。


<tips>
linux内核uImage:
'make menuconfig---> Kernel hacking ---> show timing information on printks
当选中这个选项后,启动内核,会在日志信息前面加上时间戳。取消选中,即可取消时间戳。
0 0