Linux设备管理
1.注册创建字符设备
1.1注册字符设备号
- 先调用register_chrdev()注册设备(提交file_operation结构体)
- 调用class_device()创建类用于相同类型的设备管理
- 调用device_create(),将注册的设备归于某一类下,传入设备号,这样才能找到第一步注册的设备。

流程图:

udev是用户空间的一个应用程序,在内核空间中安装驱动时,驱动会向用户空间提供信息,向上提交目录名,在这个目录下提交设备信息,当提交信息时,后台运行的hotplug会监测/sys/class/目录/信息,当产生新的信息时,hotplug会通知udev,udev会在/dev下创建节点。
1.注册设备
linux内核的字符设备号注册有两个函数,register_chrdev_region和alloc_chrdev_region。
区别:
register_chrdev_region:需要开发者指定设备的主设备号
alloc_chrdev_region:由内核自动分配主设备号。
两种申请设备号的注销方式都是通过unregister_chrdev_region进行的。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name); // 调用__register_chrdev_region函数分配设备号
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name); // 调用__register_chrdev_region函数分配设备号
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
分析: 可以看到两个函数的核心都是调用__register_chrdev_region去分配主设备号。但不同的是register_chrdev_region传入的是开发者传递的主设备号,而alloc_chrdev_region传入的主设备号是0。
__register_chrdev_region
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, *curr, *prev = NULL;
int ret;
int i;
if (major >= CHRDEV_MAJOR_MAX) {
pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",
name, major, CHRDEV_MAJOR_MAX-1);
return ERR_PTR(-EINVAL);
}
if (minorct > MINORMASK + 1 - baseminor) {
pr_err("CHRDEV \"%s\" minor range requested (%u-%u) is out of range of maximum range (%u-%u) for a single major\n",
name, baseminor, baseminor + minorct - 1, 0, MINORMASK);
return ERR_PTR(-EINVAL);
}
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
if (major == 0) {
ret = find_dynamic_major();
if (ret < 0) {
pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",
name);
goto out;
}
major = ret;
}
ret = -EBUSY;
i = major_to_index(major);
for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) {
if (curr->major < major)
continue;
if (curr->major > major)
break;
if (curr->baseminor + curr->minorct <= baseminor)
continue;
if (curr->baseminor >= baseminor + minorct)
break;
goto out;
}
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
if (!prev) {
cd->next = curr;
chrdevs[i] = cd;
} else {
cd->next = prev->next;
prev->next = cd;
}
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
__register_chrdev_region会判断major是否为0,如果为0,find_dynamic_major就在内核维护的设备表中寻找一个空闲的位置并把该位置的下标返回给驱动。内核的设备表其实就是一个数组,该数组的最大下标为255。
linux中每一个设备都有一个设备号,设备号是由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
static struct char_device_struct {
struct char_device_struct *next; // 设备表链表
unsigned int major; // 主设备号
unsigned int baseminor; // 次设备号
int minorct; // 设备个数
char name[64]; // 设备名字
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
cat /proc/devices来查看已使用的主设备号,在使用register_chrdev_region的时候,已被内核使用的主设备号是不能拿来注册的。
cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
1.2 创建字符设备
struct cdev *cdev结构体
struct cdev {
struct kobject kobj; // 内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,即file_operations.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
} __randomize_layout;
从内核角度来看,一个cdev结构体就是一个字符设备。
在cdev 中有两个重要的成员变量:ops 和dev,这两个就是字符设备文件操作函数集合files_operations 以及设备号dev_t 。编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就是表示一个字符设备。
1.2.1 字符设备结构初始化
cdev_init
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
- 参数cdev : 就是要初始化的cdev 结构体变量。
- 参数fops : 就是字符设备文件操作函数集合。
1.2.2 cdev_add 函数
cdev_add 函数用于向Linux 系统添加字符设备(cdev 结构体变量),首先使用cdev_init 函数完成对cdev 结构体变量的初始化,然后使用 cdev_add 函数向Linux 系统添加这个字符设备。cdev_add 函数原型如下:
int cdev_add(struct cdev* p, dev_t dev , unsigned count)
- 参数p : 指向要添加的字符设备(cdev 结构体变量)
- 参数 dev : 设备所使用的设备号。
- 参数count :要添加的设备数量。
设备号申请,cdev_init ,cdev_add 一起实现了函数register_chrdev 的功能。
1.2.3 cdev_del 函数
卸载驱动的时候一定要使用cdev_del 函数从Linux 内核中删除相应的字符设备,cdev_del 函数原型如下:
void cdev_del(struct cdev* p);
参数p 就是要删除的字符设备.
2.访问字符设备的流程
文件路径=>inode=>chrdev_open()=>(kobj_lookup=>)inode.i_cdev=>cdev.fops.my_chr_open()
只需要通过VFS找到了inode,就可以找到chardev_open(),这里需要关注chrdev_open()是怎么从内核的数据结构中找到我们的cdev并执行其中的my_chr_open()的。
static int chrdev_open(struct inode *inode, struct file *filp)
352 {
353 const struct file_operations *fops;
354 struct cdev *p;
355 struct cdev *new = NULL;
356 int ret = 0;
...
359 p = inode->i_cdev;
360 if (!p) {
361 struct kobject *kobj;
362 int idx;
...
364 kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
...
367 new = container_of(kobj, struct cdev, kobj);
369 /* Check i_cdev again in case somebody beat us to it while
370 we dropped the lock. */
371 p = inode->i_cdev;
372 if (!p) {
373 inode->i_cdev = p = new;
374 list_add(&inode->i_devices, &p->list);
375 new = NULL;
376 } else if (!cdev_get(p))
377 ret = -ENXIO;
378 } else if (!cdev_get(p))
379 ret = -ENXIO;
...
386 fops = fops_get(p->ops);
...
390 replace_fops(filp, fops);
391 if (filp->f_op->open) {
392 ret = filp->f_op->open(inode, filp);
...
395 }
396
397 return 0;
398
399 out_cdev_put:
400 cdev_put(p);
401 return ret;
402 }
chrdev_open()
--359-->尝试将inode->i_cdev(一个cdev结构指针)保存在局部变量p中,
--360-->如果p为空,即inode->i_cdev为空,
--364-->我们就根据inode->i_rdev(设备号)通过kobj_lookup()搜索cdev_map,并返回与之对应kobj,
--367-->由于kobject是cdev的父类,我们根据container_of很容易找到相应的cdev结构并将其保存在inode->i_cdev中,
--374-->找到了cdev,我们就可以将inode->devices挂接到inode->i_cdev的管理链表中,这样下次就不用重新搜索,
--378-->直接cdev_get()即可。
--386-->找到了我们的cdev结构,我们就可以将其中的操作方法集inode->i_cdev->ops传递给filp->f_ops(386-390),
--392-->这样,我们就可以回调我们的设备打开函数my_chr_open();如果我们没有实现自己的open接口,就什么都不做,也不是错
3.字符设备与sysfs
字符设备注册过程中没有初始化cdev.kobj的函数,我们都是通过cdev_map来管理系统里的字符设备的,所以,我们并不能在sysfs中找到注册的字符设备,更深层次的原因是内核中并不能直接使用cdev作为一个设备,而是将其作为一个设备接口,使用这个接口我们可以派生出misc设备,输入设备,lcd等等。
Linux中几乎所有的设备都是device的子类,无论是平台设备还是i2c设备还是网络设备。但唯独字符设备不是,可以看出cdev并不是继承自device,注册一个cdev对象到内核其实只是将它放到cdev_map中。
cdev更合适理解为一种接口,而不是一个具体的设备。
4.创建设备节点
字符设备创建完成后不会自动生成设备节点
4.1 手动创建设备节点
使用mknod命令创建一个设备节点
mknod 名称 类型 主设备号 次设备号
这里的类型一般为c,次设备号为0,主设备号是动态分配好的。下面例子将使用如下命令进行创建。
mknod /dev/test c 242 0
可以在启动脚本里添加如上:
4.2 自动创建设备节点
1.创建类
创建类的定义在/linux-4.1.15/include/linux/device.h中。
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
2.删除类
extern void class_destroy(struct class *cls);
在/sys/class目录下可以查看创建的类。
3.在类下创建设备:
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
device_create是个可变参数函数。
- 第一个参数:设备注册在该类下。
- 第二个参数:父设备,一般为NULL。
- 第三个参数:设备号。
- 第四个参数:设备可能会使用的数据,一般为NULL。
- 第五个参数:设备名字,生成后会在/dev目录下。
其返回值就是创建好的设备
4.删除/注销设备:
extern void device_destroy(struct class *cls, dev_t devt);
- 第一个参数:类名称。
- 第二个参数:设备号。

浙公网安备 33010602011771号