1 Linux内核模块

内核模块具有这样的特点。

  • 模块本身不被编译入内核映像, 从而控制了内核的大小。
  • 模块一旦被加载, 它就和内核中的其他部分完全一样。

    1.1 内核模块的例子

    如下所示最简单的内核模块代码: ```c /*
    • simple kernel module: hello */

      include

      include

MODULE_AUTHOR(“BARRET REN”); MODULE_LICENSE(“GPL v2”); MODULE_DESCRIPTION(“A simaple hello world module”); MODULE_ALIAS(“a simplest module”);

static int __init hello_init(void) { printk(KERN_INFO “Hello world enter\n”); return 0; }

static void __exit hello_exit(void) { printk(KERN_INFO “Hello world exit\n”); }

//模块初始化和注销函数指定 module_init(hello_init); module_exit(hello_exit);

  1. 使用下面的Makefile文件编译源代码,会生成**KO**的内核模块文件:
  2. ```makefile
  3. #内核module的路径,如果是自己下载的kernel源码,需要直接指定路径
  4. KVERS = $(shell uname -r)
  5. # Kernel modules
  6. obj-m := hello.o
  7. #多文件时指定该参数单文件不用加
  8. modulename-objs := file1.o file2.o
  9. # Specify flags for the module compilation.
  10. #EXTRA_CFLAGS=-g -O0 包含调试信息
  11. build: kernel_modules
  12. kernel_modules:
  13. make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
  14. clean:
  15. make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

内核模块文件主要用到以下几个命令操作:

  • sudo insmod ./1_hello.ko:加载内核模块
  • sudo rmmod 1_hello:卸载内核模块
  • modinfo 1_hello.ko:查看模块的具体信息,包括模块作者、 模块的说明、 模块所支持的参数以及vermagic
  • dmesg:查看模块的打印信息
  • lsmod:查看系统中已加载的所有模块以及模块间的依赖关系

内核中已加载模块的信息也存在于/sys/module目录下, 加载hello.ko后, 内核中将包含/sys/module/hello目录,

1.2 内核模块程序结构

一个Linux内核模块主要由如下几个部分组成:

  • 加载函数:当通过insmod或modprobe命令加载内核模块时, 模块的加载函数会自动被内核执行
  • 卸载函数:当通过rmmod命令卸载某模块时, 模块的卸载函数会自动被内核执行
  • 许可证声明:描述内核模块的许可权限, 如果不声明LICENSE, 模块被加载时, 将收到内核被污染(Kernel Tainted)的警告。可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”
  • 模块参数(可选):模块被加载的时候可以传递给它的值, 它本身对应模块内部的全局变量
  • 模块导出符号(可选):内核模块可以导出的符号(symbol, 对应于函数或变量),其他模块则可以使用本模块中的变量或函数
  • 作者信息等(可选)

加载和卸载函数

加载其他模块:在Linux内核中, 可以使用request_module(const char*fmt, …) 函数加载内核模块, 驱动开发人员可以通过调用下列代码:request_module(module_name);灵活地加载其他内核模块。

所有的init函数在区段.initcall.init中还保存了一份函数指针, 在初始化时内核会通过这些函数指针调用这些init函数, 并在初始化完成后, 释放init区段(包括.init.text、 .initcall.init等) 的内存。数据也可以被定义为__initdata, 对于只是初始化阶段需要的数据, 内核在初始化完后, 也可以释放它们占用的内存:

  1. static int hello_data __initdata = 1;
  2. static int __init hello_init(void)
  3. {
  4. printk(KERN_INFO "Hello, world %d\n", hello_data);
  5. return 0;
  6. }
  7. module_init(hello_init);

同样,只是退出阶段采用的数据也可以用__exitdata来形容。

模块参数

我们可以用“module_param(参数名, 参数类型, 参数读/写权限) ”为模块定义一个参数,例如:

  1. static int book_num = 4000;
  2. module_param(book_num, int, S_IRUGO); //参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针) 、bool或invbool(布尔的反)
  • 在装载内核模块时, 用户可以向模块传递参数, 形式为“insmode(或modprobe)模块名 参数名=参数值”, 如果不传递, 参数将使用模块内定义的缺省值。
  • 如果模块被内置, 就无法insmod了, 但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数

模块也可以拥有参数数组, 形式为“module_param_array(数组名, 数组类型, 数组长, 参数读/写权限)”,运行insmod或modprobe命令时, 应使用逗号分隔输入的数组元素。

代码示例如下:

  1. /*
  2. * 带模块参数的内核模块
  3. */
  4. #include <linux/init.h>
  5. #include <linux/module.h>
  6. MODULE_AUTHOR("BARRET REN");
  7. MODULE_LICENSE("GPL v2");
  8. MODULE_DESCRIPTION("A simaple module with params");
  9. MODULE_ALIAS("a simplest module");
  10. //添加模块参数定义
  11. static char* book_name = "Linux device Driver";
  12. module_param(book_name, charp, S_IRUGO);
  13. static int book_num = 4000;
  14. module_param(book_num, int, S_IRUGO);
  15. static int __init hello_init(void)
  16. {
  17. printk(KERN_INFO "book name :%s\n", book_name);
  18. printk(KERN_INFO "book num :%d\n", book_num);
  19. return 0;
  20. }
  21. static void __exit hello_exit(void)
  22. {
  23. printk(KERN_INFO "Hello world exit\n");
  24. }
  25. //模块初始化和注销函数指定
  26. module_init(hello_init);
  27. module_exit(hello_exit);

模块导出符号

Linux的“/proc/kallsyms”文件对应着内核符号表, 它记录了符号以及符号所在的内存地址,模块可以使用如下宏导出符号到内核符号表:

  1. EXPORT_SYMBOL( 符号名 );
  2. EXPORT_SYMBOL_GPL( 符号名 ); //EXPORT_SYMBOL_GPL() 只适用于包含GPL许可权的模块

导出的符号可以被其他模块使用, 只需使用前声明一下即可。卸载模块时,符号表中的符号也会被删除,代码示例:

  1. /*
  2. * 导出函数符号到内核参数表
  3. */
  4. #include <linux/init.h>
  5. #include <linux/module.h>
  6. int add_integar(int a, int b)
  7. {
  8. return a + b;
  9. }
  10. EXPORT_SYMBOL_GPL(add_integar);
  11. int sub_integar(int a, int b)
  12. {
  13. return a - b;
  14. }
  15. EXPORT_SYMBOL_GPL(sub_integar);
  16. MODULE_LICENSE("GPL v2");

1.3 模块使用计数

Linux 2.6以后的内核为不同类型的设备定义了struct module* owner域, 用来指向管理此设备的模块。 当开始使用某个设备时,内核使用try_module_get(dev->owner)去增加管理此设备的owner模块的使用计数; 当不再使用此设备时, 内核使用module_put(dev->owner)减少对管理此设备的管理模块的使用计数。 这样, 当设备在使用时, 管理此设备的模块将不能被卸载。 只有当设备不再被使用时, 模块才允许被卸载。

对于设备驱动而言, 很少需要亲自调用try_module_get()与module_put(),对设备owner模块的计数管理由内核里更底层的代码(如总线驱动或是此类设备共用的核心模块) 来实现。

2 Linux文件系统

在设备驱动程序的设计中, 一般而言, 会关心file和inode这两个结构体

2.1 file结构体

file结构体代表一个打开的文件, 系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建, 并传递给在文件上进行操作的任何函数。 在文件的所有实例都关闭后, 内核释放这个数据结构。

文件读/写模式mode、 标志f_flags都是设备驱动关心的内容 ,私有数据指针private_data在设备驱动中被广泛应用, 大多被指向设备驱动自定义以用于描述设备的结构体。

file结构体的定义如下:

  1. //include/linux/fs.h文件中
  2. struct file {
  3. union {
  4. struct llist_node fu_llist;
  5. struct rcu_head fu_rcuhead;
  6. } f_u;
  7. struct path f_path;
  8. struct inode *f_inode; /* cached value */
  9. const struct file_operations *f_op; //和文件关联的操作
  10. /*
  11. * Protects f_ep_links, f_flags.
  12. * Must not be taken from IRQ context.
  13. */
  14. spinlock_t f_lock;
  15. enum rw_hint f_write_hint;
  16. atomic_long_t f_count;
  17. unsigned int f_flags; //文件标志,如O_RDONLY,O_NONBLOCK,O_SYNC
  18. fmode_t f_mode; //文件读写权限,FMODE_READ和FMODE_WRITE
  19. struct mutex f_pos_lock;
  20. loff_t f_pos; //当前读写位置
  21. struct fown_struct f_owner;
  22. const struct cred *f_cred;
  23. struct file_ra_state f_ra;
  24. u64 f_version;
  25. #ifdef CONFIG_SECURITY
  26. void *f_security;
  27. #endif
  28. /* needed for tty driver, and maybe others */
  29. void *private_data; //文件私有数据
  30. #ifdef CONFIG_EPOLL
  31. /* Used by fs/eventpoll.c to link all the hooks to this file */
  32. struct list_head f_ep_links;
  33. struct list_head f_tfile_llink;
  34. #endif /* #ifdef CONFIG_EPOLL */
  35. struct address_space *f_mapping;
  36. errseq_t f_wb_err;
  37. } __randomize_layout
  38. __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

2.2 inode结构体

inode包含文件访问权限、 属主、 组、 大小、 生成时间、 访问时间、 最后修改时间等信息。 它是Linux管理文件系统的最基本单位, 也是文件系统连接任何子目录、 文件的桥梁。主要属性如下:

  1. struct inode {
  2. umode_t i_mode; //inode权限
  3. unsigned short i_opflags;
  4. kuid_t i_uid;//拥有者id
  5. kgid_t i_gid;//拥有者组id
  6. unsigned int i_flags;
  7. ......
  8. dev_t i_rdev;//若是设备文件, 此字段将记录设备的设备号
  9. loff_t i_size;//文件大小
  10. struct timespec i_atime;//最后一次存取时间
  11. struct timespec i_mtime;//最近一次修改时间
  12. struct timespec i_ctime;//创建时间
  13. ......
  14. blkcnt_t i_blocks;//inode使用的block数,一个block为512字节
  15. ......
  16. union {
  17. struct pipe_inode_info *i_pipe;
  18. struct block_device *i_bdev; //若是块设备, 为其对应的 block_device 结构体指针
  19. struct cdev *i_cdev;//若是字符设备, 为其对应的 cdev 结构体指针
  20. char *i_link;
  21. unsigned i_dir_seq;
  22. };
  23. ......
  24. } __randomize_layout;

i_rdev字段包含设备编号。 Linux内核设备编号分为主设备编号和次设备编号, 前者为dev_t的高12位, 后者为dev_t的低20位。如下函数可以获取主次设备号:

  1. unsigned int iminor(struct inode *inode);
  2. unsigned int imajor(struct inode *inode);

3 udev设备管理

kernel 2.4引入的devfs,在Linux 2.6内核中,被认为是过时的方法,并最终被抛弃,由udev代替。

udev完全在用户态工作,利用设备加入或移除时内核所发送的热插拔事件(Hotplug Event,监听PF_NETLINK socket,代码示例如下)来工作,可以根据系统中硬件设备状态来创建或者删除设备文件。
udev的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用从内核收到的信息来进行创建设备文件节点等工作。

注:在嵌入式系统中, 也可以用udev的轻量级版本mdev, mdev集成于busybox中。 在编译busybox的时候, 选中mdev相关项目即可。另外Android采用的是vold,和udev一样的机制,也是监听netlink套接字。

对于冷插拔的设备, Linux内核提供了sysfs下面一个uevent节点, 可以往该节点写一个“add”, 导致内核重新发送netlink,之后udev就可以收到冷插拔的netlink消息了。

  1. //netlink套接字监听热插拔事件
  2. #include <linux/netlink.h>
  3. #include <sys/poll.h>
  4. #include <stdlib.h>
  5. #include <stdio.h>
  6. #include <string.h>
  7. #include <unistd.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. static void die(char* s)
  11. {
  12. write(2, s, strlen(s));
  13. exit(1);
  14. }
  15. int main()
  16. {
  17. struct sockaddr_nl nls;
  18. struct pollfd pfd;
  19. char buf[512];
  20. memset(&nls, 0, sizeof(struct sockaddr_nl));
  21. pfd.events = POLLIN; //设置监听插入事件
  22. pfd.fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
  23. if (pfd.fd == -1)
  24. die("not root\n");
  25. nls.nl_family = AF_NETLINK;//设置netlink监听,不是普通socket的ip类型
  26. nls.nl_pid = getpid();
  27. nls.nl_groups = -1;
  28. if (bind(pfd.fd, (void*)&nls, sizeof(struct sockaddr_nl)))
  29. die("bind socket failed\n");
  30. while(poll(&pfd, 1, -1))
  31. {
  32. int i, len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
  33. if (len == -1)
  34. die("recv nothing\n");
  35. i = 0;
  36. while(i < len)
  37. {
  38. printf("%s\n", buf+i);
  39. i+= strlen(buf+i) + 1;
  40. }
  41. }
  42. die("poll\n");
  43. return 0;
  44. }

sysfs文件系统

Linux 2.6以后的内核引入了sysfs文件系统, sysfs被看成是与proc、 devfs和devpty同类别的文件系统, 该文件系统是一个虚拟的文件系统, 它可以产生一个包括所有系统硬件的层级视图, 与提供进程和状态信息的proc文件系统十分类似。挂载位置为/sys。sysfs把连接在系统上的设备和总线组织成为一个分级的文件, 它们可以由用户空间存取, 向用户空间导出内核数据结构以及它们的属性。
sysfs中的目录来源于bus_type、device_driver device, 而目录中的文件则来源于对应的attribute结构体。

在Linux内核中, 分别使用bus_type、 device_driver和device来描述总线、 驱动和设备, 这3个结构体定义于include/linux目录中。驱动和设备在内核中时分开注册的,最后通过bus_type中的match()函数绑定在一起。

udev工作过程

  • 当内核检测到系统中出现了新设备后, 内核会通过netlink套接字发送uevent。
  • udev获取内核发送的信息, 进行规则的匹配。 匹配的事物包括SUBSYSTEM、ACTION、atttribute、内核提供的名称(通过KERNEL=) 以及其他的环境变量(我们可以根据这些信息, 创建一个规则, 以便每次插入设备的时候,添加一个设备文件)。

    udev规则文件

    udev的规则文件以行为单位, 以“#”开头的行代表注释行。 其余的每一行代表一个规则。 每个规则分成一个或多个匹配部分和赋值部分。

  • 匹配部分关键字: ACTION(行为)、KERNEL(匹配内核设备名)、BUS(匹配总线类型)、SUBSYSTEM(匹配子系统名)、ATTR(属性)

  • 赋值部分关键字: NAME(创建的设备文件名)、SYMLINK(符号创建链接名)、OWNER(设置设备的所有者)、GROUP(设置设备的组)、 IMPORT(调用外部程序)、 MODE(节点访问权限)

规则文件示例:

  1. # 当系统中出现的新硬件属于net子系统范畴, 系统对该硬件采取的动作是“add”这个硬件, 且这个硬件的“address”属性信息等于“08:00:27:35:be:ff”, “dev_id”属性等于“0x0”、 “type”属性为1
  2. SUBSYSTEM=="net", ACTION=="add", DRIVERS==" *", ATTR{address}=="08:00:27:35:be:ff", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="eth*", NAME="eth1"

4 udev点灯示例代码

下面是一个简单的开发板上led点灯代码,主要用到了如下两个知识点:

  • 定义有关寄存器物理地址,然后使用ioremap函数进行内存映射,得到对应的虚拟地址,最后操作寄存器对应的虚拟地址完成对 GPIO 的初始化
  • udev机制在加载模块时自动创建设备文件 ```c

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

include

include

include

define NEWCHRLED_CNT 1 / 设备号个数 /

define NEWCHRLED_NAME “newchrled” / 名字 /

define LEDOFF 0 / 关灯 /

define LEDON 1 / 开灯 /

/ 寄存器物理地址 /

define CCM_CCGR1_BASE (0X020C406C)

define SW_MUX_GPIO1_IO03_BASE (0X020E0068)

define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)

define GPIO1_DR_BASE (0X0209C000)

define GPIO1_GDIR_BASE (0X0209C004)

/ 映射后的寄存器虚拟地址指针 / static void iomem *IMX6U_CCM_CCGR1; static void iomem SW_MUX_GPIO1_IO03; static void __iomem SW_PAD_GPIO1_IO03; static void iomem *GPIO1_DR; static void iomem *GPIO1_GDIR;

/ newchrled设备结构体 / struct newchrled_dev{ dev_t devid; / 设备号 / struct cdev cdev; / cdev / struct class class; // struct device device; / 设备 / int major; / 主设备号 / int minor; / 次设备号 / };

struct newchrled_dev newchrled; / led设备 /

/*

  • @description : LED打开/关闭
  • @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
  • @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON) {
    1. val = readl(GPIO1_DR);
    2. val &= ~(1 << 3);
    3. writel(val, GPIO1_DR);
    }else if(sta == LEDOFF) {
    1. val = readl(GPIO1_DR);
    2. val|= (1 << 3);
    3. writel(val, GPIO1_DR);
    }
    }

/*

  • @description : 打开设备
  • @param - inode : 传递给驱动的inode
  • @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
  • 一般在open的时候将private_data指向设备结构体。
  • @return : 0 成功;其他 失败 / static int led_open(struct inode inode, struct file filp) { filp->private_data = &newchrled; / 设置私有数据 */ return 0; }

/*

  • @description : 从设备读取数据
  • @param - filp : 要打开的设备文件(文件描述符)
  • @param - buf : 返回给用户空间的数据缓冲区
  • @param - cnt : 要读取的数据长度
  • @param - offt : 相对于文件首地址的偏移
  • @return : 读取的字节数,如果为负值,表示读取失败 / static ssize_t led_read(struct file filp, char __user buf, size_t cnt, loff_t offt) { return 0; }

/*

  • @description : 向设备写数据
  • @param - filp : 设备文件,表示打开的文件描述符
  • @param - buf : 要写给设备写入的数据
  • @param - cnt : 要写入的数据长度
  • @param - offt : 相对于文件首地址的偏移
  • @return : 写入的字节数,如果为负值,表示写入失败 / static ssize_t led_write(struct file filp, const char __user buf, size_t cnt, loff_t offt) { int retvalue; unsigned char databuf[1]; unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt); if(retvalue < 0) {

    1. printk("kernel write failed!\r\n");
    2. return -EFAULT;

    }

    ledstat = databuf[0]; / 获取状态值 /

    if(ledstat == LEDON) {

    1. led_switch(LEDON); /* 打开LED灯 */

    } else if(ledstat == LEDOFF) {

    1. led_switch(LEDOFF); /* 关闭LED灯 */

    } return 0; }

/*

  • @description : 关闭/释放设备
  • @param - filp : 要关闭的设备文件(文件描述符)
  • @return : 0 成功;其他 失败 / static int led_release(struct inode inode, struct file *filp) { return 0; }

/ 设备操作函数 / static struct file_operations newchrled_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, };

/*

  • @description : 驱动出口函数
  • @param : 无
  • @return : 无 */ static int __init led_init(void) { u32 val = 0;

    / 初始化LED / / 1、寄存器地址映射 / IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4); GPIO1_DR = ioremap(GPIO1_DR_BASE, 4); GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    / 2、使能GPIO1时钟 / val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); / 清楚以前的设置 / val |= (3 << 26); / 设置新值 / writel(val, IMX6U_CCM_CCGR1);

    /* 3、设置GPIO1_IO03的复用功能,将其复用为

    • GPIO1_IO03,最后设置IO属性。 */ writel(5, SW_MUX_GPIO1_IO03);

      /寄存器SW_PAD_GPIO1_IO03设置IO属性 bit 16:0 HYS关闭 bit [15:14]: 00 默认下拉 bit [13]: 0 kepper功能 bit [12]: 1 pull/keeper使能 bit [11]: 0 关闭开路输出 bit [7:6]: 10 速度100Mhz bit [5:3]: 110 R0/6驱动能力 bit [0]: 0 低转换率 / writel(0x10B0, SW_PAD_GPIO1_IO03);

      / 4、设置GPIO1_IO03为输出功能 / val = readl(GPIO1_GDIR); val &= ~(1 << 3); / 清除以前的设置 / val |= (1 << 3); / 设置为输出 / writel(val, GPIO1_GDIR);

      / 5、默认关闭LED / val = readl(GPIO1_DR); val |= (1 << 3);
      writel(val, GPIO1_DR);

      / 注册字符设备驱动 / / 1、创建设备号 / if (newchrled.major) { / 定义了设备号 / newchrled.devid = MKDEV(newchrled.major, 0); register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME); } else { / 没有定义设备号 / alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); / 申请设备号 / newchrled.major = MAJOR(newchrled.devid); / 获取分配号的主设备号 / newchrled.minor = MINOR(newchrled.devid); / 获取分配号的次设备号 / } printk(“newcheled major=%d,minor=%d\r\n”,newchrled.major, newchrled.minor);

      / 2、初始化cdev / newchrled.cdev.owner = THIS_MODULE; cdev_init(&newchrled.cdev, &newchrled_fops);

      / 3、添加一个cdev / cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

      / 4、创建类 / newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); if (IS_ERR(newchrled.class)) { return PTR_ERR(newchrled.class); }

      / 5、创建设备 / newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME); if (IS_ERR(newchrled.device)) { return PTR_ERR(newchrled.device); }

      return 0; }

/*

  • @description : 驱动出口函数
  • @param : 无
  • @return : 无 / static void __exit led_exit(void) { / 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR);

    / 注销字符设备驱动 / cdev_del(&newchrled.cdev);/ 删除cdev / unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); / 注销设备号 /

    device_destroy(newchrled.class, newchrled.devid); class_destroy(newchrled.class); }

module_init(led_init); module_exit(led_exit); MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“barretren”); ```