字符设备是内核的接口的一种,即可以通过字符设备进行用户空间和内核空间的交互,这里侧重讲下字符设备,对模块知识及内核态用户态略微提及,后面会单独发相关的帖。
Linux 内核整体结构很庞大,包含了很多的组件,目前最多的也是最好的处理方式:将需要的功能编译成模块,在需要的时候动态地加载,包含进内核当中。这也就是所谓的模块化。
常用操作:
insmod 将模块添加进内核 rmmod 将模块从内核卸载 lsmod 查看已安装到内核的模块 modprobe 载入指定的个别模块,或是载入一组相依赖的 模块。 modprobe 会根据 depmod 所产生的依赖关系,决 定要载入哪些模块。若在载入 过程中发生错误,在 modprobe 会卸载整组的模块。依赖关系是通过读取 /lib/modules/2.6.xx/modules.dep 得到的。而该文件是通过 depmod 所建立。 modinfo 查看模块信息。使用方法:modinfo XXX.ko tree -a 查看当前目录的整个树结构
注意:这些操作需要超级用户权限,即root权限。
定义: 字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
helloworld.c 废话不多说,show code:
#include <linux/module.h> #include <linux/moduleparam.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/wait.h> #include <linux/poll.h> #include <linux/sched.h> #include <linux/slab.h> #define BUFFER_MAX (32) #define SUCCESS (0) #define ERROR (-1) struct cdev *gDev;//使用cdev结构体来描述字符设备 struct file_operations *gFile;//通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等 dev_t devNum;//通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性 unsigned int subDevNum = 1;//请求的连续设备个数 int reg_major = 813;//主设备号 int reg_minor = 0; //次设备号 char *buffer; /*定义内核相关操作函数open()、read()、write()*/ int hello_open(struct inode *p, struct file *f) { printk(KERN_INFO "hello_open\r\n"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_write\r\n"); return 0; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_read\r\n"); return 0; } /*初始化函数*/ int hello_init(void) { devNum = MKDEV(reg_major, reg_minor);//通过主次设备号生成设备号 if(SUCCESS == register_chrdev_region(devNum, subDevNum, "helloworld")){ printk(KERN_INFO "register_chrdev_region ok \n"); } else { printk(KERN_INFO "register_chrdev_region error n"); return ERROR; } printk(KERN_INFO " hello driver init \n"); gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); gFile->open = hello_open; gFile->read = hello_read; gFile->write = hello_write; gFile->owner = THIS_MODULE; cdev_init(gDev, gFile); cdev_add(gDev, devNum, 1); return 0; } void __exit hello_exit(void) { printk(KERN_INFO " hello driver exit \n"); cdev_del(gDev); kfree(gFile); kfree(gDev); unregister_chrdev_region(devNum, subDevNum); return; } /*模块操作相关*/ module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");代码里已添加了简单注释说明,为了增加可读性,一些需要自行查阅相关书籍或搜索学习。(可直接复制使用)
Makefile 直接上代码:
ifneq ($(KERNELRELEASE),) obj-m := helloworld.o else PWD := $(shell pwd) KDIR:= /lib/modules/3.10.0-1127.el7.x86_64/build #KDIR := /lib/modules/`uname -r`/build all: make -C $(KDIR) M=$(PWD) clean: rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~ endifmakefile我学过一些,和用户态的makefile有些差异,这里不再详细说明,只把需要注意的一些地方说一下: 1、开始KERNELRELEASE并未定义,将进入else并顺序执行到最后,另外内核里定义了KERNELRELEASE,makefile文件将执行第二次,将执行obj-m := helloDev.o,进行编译。 2、KDIR这里指向编译模块的架构,你可以通过命令:uname -r查看。
展示: 未编译: 编译: 将.c文件和makefile文件放在同一路径即可,编译后可以看见生成的,.o、.ko等文件,还有build-in.o(感兴趣可学习下),再输入命令:make clean。就可以执行rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~了。
通过上述操作已经生成了我们需要的ko文件。 下面就是加载到内核了,前面说过这个操作需要超级管理员权限。 通过命令: insmod helloworld.ko 即可插入到内核。
如何查看是否插入成功呢? 1、lsmod 2、dmesg 可以看到helloworld.c代码里的打印。 另外,可以用dmesg -c 先清除下。
Linux下的设备都在/dev目录下 如何使用这些驱动呢? 需要mknod 命令建立一个目录项和一个特殊文件的对应索引节点(即字符设备文件)
使用说明: mknod [options] name {bc} major minor name:设备名称 {bc}:设备类型 p FIFO型 b 块文件 c 字符文件 major、minor主次设备号。 ps:由root 用户或系统组成员运行。
创建设备节点: mknod /dev/hello c 813 0 可以看到创建的字符设备节点hello。 后面可以使用节点hello来测试了。
我们这里写一个用户态的程序测试下前面的内核代码(ko文件)。 内核态、用户态如何交互呢,这里不详细说明了,简单po张图来展示下。 字符设备、字符设备驱动与用户空间的关系 test.c
#include <fcntl.h> #include <stdio.h> #include <string.h> #include <sys/select.h> #define DATA_NUM 64 int main(int argc,char *argv[]){ int fd,i; int r_len,w_len; fd_set fdset; char buf[DATA_NUM] = "hello world"; memset(buf,0,sizeof(buf)); fd = open("/dev/hello",O_RDWR);//通过open函数打开之前的字符设备节点 printf("%d\r\n",fd); if(-1 == fd){ perror("open file error\r\n"); return -1; } else{ printf("open successe\r\n"); } w_len = write(fd,buf,DATA_NUM);//调用内核的write函数 r_len = read(fd,buf,DATA_NUM); printf("%d %d \r\n",w_len,r_len); printf("%s\r\n",buf); return 0; }当然这里没有做太多打印,只是测试下,说明这样一个过程,后面有时间优化下代码,然后再展示说明。
《Linux设备驱动程序》第三版 b站:技术简说