字符设备是内核的接口的一种,即可以通过字符设备进行用户空间和内核空间的交互,这里侧重讲下字符设备,对模块知识及内核态用户态略微提及,后面会单独发相关的帖。
1、模块化
Linux 内核整体结构很庞大,包含了很多的组件,目前最多的也是最好的处理方式:将需要的功能编译成模块,在需要的时候动态地加载,包含进内核当中。这也就是所谓的模块化。
常用操作:
insmod 将模块添加进内核
rmmod 将模块从内核卸载
lsmod 查看已安装到内核的模块
modprobe 载入指定的个别模块,或是载入一组相依赖的 模块。 modprobe 会根据 depmod 所产生的依赖关系,决 定要载入哪些模块。若在载入 过程中发生错误,在 modprobe 会卸载整组的模块。依赖关系是通过读取 /lib/modules/2.6.xx/modules.dep 得到的。而该文件是通过 depmod 所建立。
modinfo 查看模块信息。使用方法:modinfo XXX.ko
tree -a 查看当前目录的整个树结构
注意:这些操作需要超级用户权限,即root权限。
2、字符驱动设备
定义:
字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和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~ *~
endif
makefile我学过一些,和用户态的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~ *~了。
3、模块加载、卸载等
通过上述操作已经生成了我们需要的ko文件。
下面就是加载到内核了,前面说过这个操作需要超级管理员权限。
通过命令:
insmod helloworld.ko
即可插入到内核。
如何查看是否插入成功呢?
1、lsmod
2、dmesg
可以看到helloworld.c代码里的打印。
另外,可以用dmesg -c 先清除下。
4、字符设备映射
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来测试了。
5、应用层程序测试
我们这里写一个用户态的程序测试下前面的内核代码(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站:技术简说
http://blog.sina.com.cn/s/blog_7da9e4aa0101lrp9.html