Linux内核大讲堂 (二) 传说中的字符设备(2)
这一节我们先给出一个字符设备的小例子,源码结构如下:
|-- wwhs_chardev
| |-- Makefile
| |-- wwhs_chardrv.c //驱动
| `-- wwhs_chardrv_test.c //小测试程序
`-- wwhs_public.h
请大家养成看Makefile的好习惯,这个Makefile很简单的。
先生成字符设备的驱动,字符设备驱动会在/dev/目录下生成一个wwhs_chardev的节点。然后再生成测试小程序,小测试程序就是打开这个节点,然后往节点里写几个字符,然后再读出来。
先给出源码吧:
Makefile:
obj-m+=wwhs_chardrv.o
KERNELDIR=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:chardrv test
chardrv:
make -C $(KERNELDIR) M=$(PWD) modules
test:
gcc wwhs_chardrv_test.c -g -o wwhs_chardrv_test
clean:
rm -rf *.o* *.ko* *.mod.c *.cmd *.symvers .tmp_versions .*.cmd wwhs_chardrv_test
wwhs_chardrv.c:
#include "../wwhs_public.h"
#define WWHS_MAJOR 247
#define WWHS_CHARDEV_NAME "wwhs_chardev"
#define WWHS_MAX_BUFFER 256
#define WWHS_CLASS_NAME "wwhs_class"
static int wwhs_open(struct inode *inode, struct file *file)
{
printk("%s/n",__func__);
return 0;
}
static char *wwhs_charname;
ssize_t wwhs_read(struct file *file, char __user *user, size_t size, loff_t *offset)
{
int wwhs_size = strlen(wwhs_charname) + 1;
if(copy_to_user(user,wwhs_charname,wwhs_size))
return 0;
*offset = wwhs_size;
printk("%s/n",__func__);
return wwhs_size;
}
ssize_t wwhs_write(struct file *file, char __user *user, size_t size, loff_t *offset)
{
if (size < 1)
return -EINVAL;
if (wwhs_charname)
kfree(wwhs_charname);
wwhs_charname =kzalloc(size*sizeof(char),GFP_KERNEL);
if (!wwhs_charname){
printk("out of memory/n");
return -ENOMEM;
}
if(copy_from_user(wwhs_charname,user,size))
return -EFAULT;
*offset += size;
printk("%s/n",__func__);
return size;
}
static const struct file_operations wwhs_fops = {
.owner = THIS_MODULE,
.open = wwhs_open,
.read = wwhs_read,
.write = wwhs_write,
};
static struct class *wwhs_class;
static int __init wwhs_init()
{
wwhs_class = class_create(THIS_MODULE, WWHS_CLASS_NAME);
device_create(wwhs_class, NULL, MKDEV(WWHS_MAJOR, 0), NULL, WWHS_CHARDEV_NAME);
register_chrdev(WWHS_MAJOR, WWHS_CHARDEV_NAME, &wwhs_fops);
return 0;
}
static void __exit wwhs_exit()
{
if (wwhs_charname)
kfree(wwhs_charname);
device_destroy(wwhs_class,MKDEV(WWHS_MAJOR, 0));
unregister_chrdev(WWHS_MAJOR,WWHS_CHARDEV_NAME);
class_destroy(wwhs_class);
}
module_init(wwhs_init);
module_exit(wwhs_exit);
MODULE_AUTHOR("wwhs");
MODULE_DESCRIPTION("wwhs_chardev");
MODULE_LICENSE("GPL");
wwhs_chardrv_test.c:
#include
#include
#include
#include
#define WWHSCHARDEVPATH "/dev/wwhs_chardev"
#define CONTEXT "mychardevnamewwhsfdfasf124123434"
int main()
{
int fd = -1;
int ret = 0;
char buf[128] = {0};
fd = open(WWHSCHARDEVPATH,O_RDWR);
if (fd < 0) {
ret = -1;
goto error;
}
if (write(fd,CONTEXT,sizeof(CONTEXT)) < 0 ){
ret = -1;
goto ferror;
}
if (read(fd,buf,sizeof(CONTEXT)) < 0 ){
ret = -1;
goto ferror;
}
ferror:
close(fd);
error:
printf("buf:%s/n",buf);
return ret;
}
wwhs_public.h:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define wwhs_dbg(dbgbuf) printk(KERN_ERR"wwhs:%s/n",dbgbuf);
进入wwhs_chardrv目录然后:
make
insmod wwhs_chardrv.ko
ls /dev/wwhs/wwhs_chardev –l
输出如下:
[root@localhost wwhs_chardev]# ls /dev/wwhs_chardev -l
crw------- 1 root root 247, 0 May 25 15:58 /dev/wwhs_chardev
要先解释一下这中间有驱动相关的两个参数,看到247和0了吧?这就是传说中的主设备号和从设备号。关于这个玩意可是有来头的,内核为了管理这两位大侠可谓是耗费精力,有拆开他们,合并他们,有自动申请。。。。可以说这两位大侠在内核中的地位是非常高的。接下来我们就分析一下这两位大侠。
class__register()之前已经分析过了。
device_create()因为之前我偷懒,所以有的东西没有讲,这一节我补上一点点,其实之前我交代过各位同学请自行分析的,不知道大家有没有分析,如果没有分析的话,这一节我帮大家补充一下。
device_create()->device_register()->device_add()->devtmpfs_create_node()->vfs_mknod().
这个玩意就是以前各位玩过字符设备同志的最爱:mknod命令的变种。
所以以后可以不用那么土了,我们应该尽量将与驱动相关的代码统一起来在内核里面管理,只要代码写的好,完全可以把所有的任务全都在内核中处理。一者效率高,二者维护也更方便,一抓一大陀,多爽!但是现实永远没有那么美好,半调子驱动工程师写出的人不人鬼不鬼的代码,给新手留下的印象都是一堆堆相互之间没有联系的东西,形成一些所谓的“经验”。从此,大家都觉得“经验”很重要,有的东西人家不说你不知道,很多工程师把mknod这个命令封到库里面,搞的好像很高深一样,真的觉得挺好玩的。一言以概之,什么档次的人就玩什么档次的花样!
OK,劳骚也发了,打击的人肯定是一大片,但哥不在乎,哥说的是自已内心真实的想法,只要能帮助曾经和我一样迷茫的你,就够了,再大的砖头砸下来,哥也扛得住。
好了,我们继续分析一下这个传说中的东西。
vfs_path_lookup(dev_mnt->mnt_root, dev_mnt,
nodename, LOOKUP_PARENT, &nd);
dev_mnt->mnt_root看到没有?
很明显,你已经据有初始化意识了,这个玩意就是等价于/dev的。所以你创建的nd的父目录就是它。很明显你创建的节点会在/dev目录下。
大概搞清了这一条主线了!我们在/dev目录下创建了一个具有自定义名称的字符设备节点,这个字符设备拥有我们分配的主从设备号。
接下来我们回到register_chrdev()。
先给出函数原型:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
首先是:
cd = __register_chrdev_region(major, baseminor, count, name);
这个函数主要是申请了一个char_device_struct结构体,然后根据major生成的index在chrdevs这个哈希指针数组中索引到对应的指针,进行一系列的出错检查后,再执行:
cd->next = *cp;
*cp = cd;
来实现两者的绑定,并返回cd这个结构体指针。
接下来就是cdev_alloc(),这个没太多好讲的,就是分配了一个cdev的结构体,进行初始化后再返回结构体的指针。
接下来实现一些我们都很熟悉的操作:
cdev->owner = fops->owner;
cdev->ops = fops; //最重要的就是这一步了。
kobject_set_name(&cdev->kobj, "%s", name);
接下来就把cdev和用MKDEV生成的包含有主设备号信息的dev_t等一起用参数传进cdev_add()函数中。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);
unsigned i;
struct probe *p;
if (n > 255)
n = 255;
p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
if (p == NULL)
return -ENOMEM;
for (i = 0; i < n; i++, p++) {
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;
}
mutex_lock(domain->lock);
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}
上一节我们对cdev_map已经做了分析,这个函数没啥别的,就是把cdev等信息与cdev_map中的probe相关联。不过提醒一下probe又是一个链表。
大概如下图所示:
画的比较挫,我也是尝试一下,结果折腾了半天才折腾出这样一个鸟图。我狂郁闷。
再解释一下算了,probes的probe指向了我们动态分配到的probe,porbe有三个重要的成员。Next就是指向下一个probe的指针,形成一个链,而dev_t就是主从设备号的杂种。Data是一个void*指针,指向我们最重要的cdev,cdev中又包含我们在例子程序中写的wwhs_fop。
就这么简单,下一节,我们会继续讲解open、ead等是怎么调用我们实际写的驱动中的open、read函数。受不了了,得抽烟了。下节见。^_^