Linux: 设备节点创建移除过程简析

Posted JiMoKuangXiangQu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux: 设备节点创建移除过程简析相关的知识,希望对你有一定的参考价值。

文章目录

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 分析背景

本文基于 Linux 4.14 内核源码进行分析。

3. 设备节点的创建和移除

用户空间的应用程序,经常会通过 /dev/XXX 设备节点,和内核空间进行交互,如读取键盘输入、利用串口设备通信等等。那么,这些 /dev/XXX 设备节点是如何创建和删除的呢?接下来就来探讨这些细节。在不同的历史时期,Linux 设备节点的创建删除方式各不相同,本文不会一一展开。最大的变化,大概是从 devfs 变化为现今一直在使用的 devtmpfs ,本文针对 devtmpfs 进行讨论。在不同的场景下,设备节点的创建移除会有些差异,下面我们一一列举这些不同的情形。

3.1 通过 devtmpfs 创建移除设备节点

3.1.1 devtmpfs 初始化

devtmpfs 创建一个名为 "kdevtmpfs" 内核线程,然后 driver core 在添加(device_add())、移除(device_del())设备过程中 ,通过 devtmpfs 接口 devtmpfs_create_node()/devtmpfs_delete_node() ,将 设备创建、移除请求信息(struct req) 添加到内核线程 "kdevtmpfs" 的请求队列 requests ,接着唤醒内核线程 "kdevtmpfs" 处理请求,然后陷入睡眠等待直到请求处理完成;内核线程 "kdevtmpfs" 被唤醒后,逐个取出请求信息,按请求类型 创建 (vfs_mknod())、移除(vfs_unlink()) 设备节点,之后唤醒等待请求完成的线程。最后,driver core 会给用户空间设备事件监听程序(如 udevd)发送消息,让监听程序对设备事件做相应处理。

start_kernel()
	rest_init()
		pid = kernel_thread(kernel_init, NULL, CLONE_FS);

kernel_init()
	kernel_init_freeable()
		do_basic_setup()
			driver_init()
				devtmpfs_init() /* 初始化 devtmpfs */
				...
			usermodehelper_enable()	
			do_initcalls() /* init 初始化接口: 包括驱动注册加载 */
		prepare_namespace() /* 挂载 rootfs */
/* drivers/bash/devtmpfs.c */

static struct task_struct *thread;

int __init devtmpfs_init(void)

	int err = register_filesystem(&dev_fs_type); /* 注册 devtmpfs 文件系统类型 */
	...

	/* 创建 【设备节点创建请求处理线程】 */
	thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");
	if (!IS_ERR(thread)) 
		/*
		 * 等待 "kdevtmpfs" 【设备节点创建请求处理线程】 就绪:
		 * "kdevtmpfs" 线程函数 devtmpfsd() 会在挂载初始化好 devtmpfs 后,
		 * 会调用 
		 * complete(&setup_done);
		 * 宣告已经准备好接收 【设备节点创建请求处理】 了。
		 */
		wait_for_completion(&setup_done);
	 else 
		...
	


static int devtmpfsd(void *p)

	char options[] = "mode=0755";
	int *err = p;
	*err = sys_unshare(CLONE_NEWNS);
	if (*err)
		goto out;
	/* 挂载 devtmpfs 文件系统 */
	*err = sys_mount("devtmpfs", "/", "devtmpfs", MS_SILENT, options);
	if (*err)
		goto out;
	sys_chdir("/.."); /* will traverse into overmounted root */
	sys_chroot(".");
	/* 
	 * 唤醒在 devtmpfs_init() 中等待的线程: 
	 * devtmpfs_init() -> wait_for_completion(&setup_done)
	 */
	complete(&setup_done);
	while (1)  /* 处理设备节点创建请求 */
		// 后面细述
		...
	
out:
	complete(&setup_done);
	return *err;

通过 ps 命令可以查看到 kdevtmpfs 线程:

root@qemu-ubuntu:~# ps -ef | grep kdevtmpfs | grep -v grep
root        32     2  0 07:58 ?        00:00:00 [kdevtmpfs]

3.1.2 通过 devtmpfs 创建设备节点

3.1.2.1 发出设备创建请求

/* drivers/base/core.c */

int device_add(struct device *dev)

	...
	
	if (MAJOR(dev->devt)) 
		error = device_create_file(dev, &dev_attr_dev);
		...
		
		error = device_create_sys_dev_entry(dev);
		...

		/* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点创建 请求 */
		devtmpfs_create_node(dev);
	

	...

添加创建请求:

/* drivers/base/devtmpfs.c */

int devtmpfs_create_node(struct device *dev)

	if (!thread) /* "kdevtmpfs" 内核线程尚未就绪 */
		return 0;

	...
	req.name = device_get_devnode(dev, &req.mode, &req.uid, &req.gid, &tmp);
	...

	if (req.mode == 0)
		req.mode = 0600; /* req.mode != 0 表示设备创建请求,否则是删除请求 */
	if (is_blockdev(dev))
		req.mode |= S_IFBLK;
	else
		req.mode |= S_IFCHR;

	req.dev = dev;

	init_completion(&req.done);

	wake_up_process(thread); /* 唤醒 "kdevtmpfs" 线程,处理请求 */
	wait_for_completion(&req.done); /* 等待请求处理完成 */

	kfree(tmp);

	return req.err;

3.1.2.2 处理设备创建请求

/* drivers/bash/devtmpfs.c */

static int devtmpfsd(void *p)

	...
	while (1)  /* 处理设备节点创建请求 */
		spin_lock(&req_lock);
		while (requests) 
			struct req *req = requests;
			requests = NULL;
			spin_unlock(&req_lock);
			while (req)  /* 逐个处理请求 */
				struct req *next = req->next;
				req->err = handle(req->name, req->mode,
						  req->uid, req->gid, req->dev);
				/* 
				 * 通知请求处理完成:
				 * devtmpfs_create_node() / devtmpfs_delete_node()
				 *		wait_for_completion(&req.done)
				 */
				complete(&req->done);
				req = next;
			
			spin_lock(&req_lock);
		
		/* 没有请求期间,陷入睡眠 */
		__set_current_state(TASK_INTERRUPTIBLE);
		spin_unlock(&req_lock);
		schedule();
	
	return 0;
out:
	...


static int handle(const char *name, umode_t mode, kuid_t uid, kgid_t gid,
		  struct device *dev)

	if (mode) /* 处理节点创建请求 */
		return handle_create(name, mode, uid, gid, dev);
	else  /* 处理节点删除请求 */
		return handle_remove(name, dev); // 后面展开


/* 处理设备节点创建请求 */
static int handle_create(const char *nodename, umode_t mode, kuid_t uid,
			 kgid_t gid, struct device *dev)

	struct dentry *dentry;
	struct path path;
	int err;

	dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);
	...

	/* 调用具体文件系统的 mknode 接口,如 ext4 的 ext4_mknod() */
	err = vfs_mknod(d_inode(path.dentry), dentry, mode, dev->devt);
	...
	return err;

3.1.2.3 通知用户态设备事件监听程序:设备对象添加

/* drivers/base/core.c */

int device_add(struct device *dev)

	...
	
	if (MAJOR(dev->devt)) 
		error = device_create_file(dev, &dev_attr_dev);
		...
		
		error = device_create_sys_dev_entry(dev);
		...

		/* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点创建 请求 */
		devtmpfs_create_node(dev);
	

	...

	/* 通知用户态设备事件监听程序: 添加了设备对象 */
	kobject_uevent(&dev->kobj, KOBJ_ADD);
	...

典型的用户态设备事件监听程序 udev ,在监听到添加设备事件后,按照配置的规则,做 修改设备节点权限、添加设备节点符号链接 等动作。
到此,系统启动期间,所有的设备节点创建工作已经完成,但是由于我们挂载 devtmpfs 、以及其下设备节点的创建工作,是在 rootfs 挂载前完成的,这样用户空间是无法在根目录 / 下看到这些设备节点的,所以在 rootfs 挂载完成后,系统重新挂载 devtmpfs 到了 rootfs/dev 目录,来看细节:

start_kernel()
	rest_init()
		pid = kernel_thread(kernel_init, NULL, CLONE_FS);

kernel_init()
	kernel_init_freeable()
		do_basic_setup()
			driver_init()
				devtmpfs_init() /* 初始化 devtmpfs */
				...
			usermodehelper_enable()	
			do_initcalls() /* init 初始化接口: 包括驱动注册加载 */
		prepare_namespace() /* 挂载 rootfs */
/* init/do_mounts.c */

void __init prepare_namespace(void)

	...
	mount_root(); /* 挂载根文件系统(rootfs) */
out:
	devtmpfs_mount("dev"); /* 将 devtmpfs 重新挂载到根文件系统的 /dev 目录 */
	sys_mount(".", "/", NULL, MS_MOVE, NULL);
	sys_chroot(".");

3.1.3 通过 devtmpfs 删除设备节点

3.1.3.1 发出设备移除请求

/* drivers/base/core.c */

void device_del(struct device *dev)

	...
	if (MAJOR(dev->devt)) 
		devtmpfs_delete_node(dev); /* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点创建 请求 */
		device_remove_sys_dev_entry(dev);
		device_remove_file(dev, &dev_attr_dev);
	
	...

3.1.3.2 处理设备移除请求

/* drivers/bash/devtmpfs.c */

static int handle_remove(const char *nodename, struct device *dev)

	struct path parent;
	struct dentry *dentry;
	int deleted = 0;
	int err;

	dentry = kern_path_locked(nodename, &parent);
	...

	if (d_really_is_positive(dentry)) 
		...
		if (!err && dev_mynode(dev, d_inode(dentry), &stat)) 
			...
			/* 调用具体文件系统的 unlink 接口,如 ext4 的 ext4_unlink() */
			err = vfs_unlink(d_inode(parent.dentry), dentry, NULL);
			...
		
	

	if (deleted && strchr(nodename, '/'))
		delete_path(nodename);
	return err;

3.1.3.3 通知用户态设备事件监听程序:设备对象移除

/* drivers/base/core.c */

void device_del(struct device *dev)

	...
	if (MAJOR(dev->devt)) 
		devtmpfs_delete_node(dev); /* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点移除 请求 */
		device_remove_sys_dev_entry(dev);
		device_remove_file(dev, &dev_attr_dev);
	
	...

	/* 通知用户态设备事件监听程序: 设备对象移除了 */
	kobject_uevent(&dev->kobj, KOBJ_REMOVE);
	...

典型的用户态设备事件监听程序 udev ,在监听到添加移除事件后,按照配置的规则,做 移除设备节点符号链接 等动作。

3.2 通过系统调用 sys_mknod()/sys_unlink() 创建移除设备节点

并非所有的设备节点都经由、或必须经由 driver core 创建移除,也可以通过系统调用 sys_mknod()/sys_unlink() 来完成。

3.2.1 通过系统调用 sys_mknod() 创建设备节点

/* fs/namei.c */

sys_mknod()
	sys_mknodat(AT_FDCWD, filename, mode, dev)
		/* 为 @filename 创建 dentry */
		dentry = user_path_create(dfd, filename, &path, lookup_flags);
		...
		switch (mode & S_IFMT) 
		...
		case S_IFCHR: case S_IFBLK:
			error = vfs_mknod(path.dentry->d_inode,dentry,mode,
								new_decode_dev(dev));
		...
		

int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)

	...
	error = dir->i_op->mknod(dir, dentry, mode, dev); /* 如 ext4_mknod() */
	...

3.2.2 通过系统调用 sys_mknod() 移除设备节点

sys_unlink()
	do_unlinkat(AT_FDCWD, pathname)
		error = vfs_unlink(path.dentry->d_inode, dentry, &delegated_inode)

int vfs_unlink(struct inode *dir, struct dentry *dentry, struct inode **delegated_inode)

	...
	error = dir->i_op->unlink(dir, dentry); /* ext4_unlink(), ... */
	...

4. 参考资料

https://lwn.net/Articles/331818/#:~:text=Sievers%20outlines%20the%20differences%20between%20devtmpfs%20and%20Adam,600%20for%20an%20early%20version%20of%20Richter%27s%20mini-devfs.
https://manpages.ubuntu.com/manpages/xenial/man7/udev.7.html

Linux驱动之平台设备驱动模型简析(驱动分离分层概念的建立)

 Linux设备模型的目的:为内核建立一个统一的设备模型,从而有一个对系统结构的一般性抽象描述。换句话说,Linux设备模型提取了设备操作的共同属性,进行抽象,并将这部分共同的属性在内核中实现,而为需要新添加设备或驱动提供一般性的统一接口,这使得驱动程序的开发变得更简单了,而程序员只需要去学习接口就行了。 

对于整个设备总线驱动模型的样子,如下图。简单来说,bus 负责维护注册进来的devcie 与 driver,每注册进来一个device 或者 driver 都会调用 Bus->match 函数 将device 与 driver 进行配对,并将它们加入链表,如果配对成功,调用Bus->probe或者driver->probe函数。注意:一个device 只能配对一个driver;而一个driver可以对应多个device。其它诸如devices_kset、kobject_uevent这里不关心,这个涉及的内容比较深入,暂时不去分析。

 

platform平台设备驱动是基于设备总线驱动模型的,如下图,这篇主要是记录平台设备的驱动与设备的注册匹配过程

 以上参考自

platform_bus提供platform_device_register、platform_driver_register等函数供platform_device层与platform_driver调用。

platform_driver属于驱动层,会在里面提供file_operations结构体供应用层调用、创建设备节点文件对应相应的驱动

platform_device属于设备层,会在里面提供resource资源文件供驱动层调用

 下面是一个例子,分别编写了Led_dev.c设备文件和Led_drv.c驱动文件。列出程序源码,再根据源码分析Led_dev与Led_drv注册与匹配过程

Led_dev.c的程序源码:

#include <linux/module.h>
#include <linux/version.h>

#include <linux/init.h>

#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/list.h>
#include <linux/timer.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/platform_device.h>


/* 分配/设置/注册一个platform_device */

static struct resource led_resource[] = {
    [0] = {
        .start = 0x56000050,
        .end   = 0x56000050 + 8 - 1,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = 5,
        .end   = 5,
        .flags = IORESOURCE_IRQ,
    }

};

static void led_release(struct device * dev)
{
}


static struct platform_device led_dev = {
    .name         = "myled",
    .id       = -1,
    .num_resources    = ARRAY_SIZE(led_resource),
    .resource     = led_resource,
    .dev = { 
        .release = led_release, 
    },
};

static int led_dev_init(void)
{
    platform_device_register(&led_dev);
    return 0;
}

static void led_dev_exit(void)
{
    platform_device_unregister(&led_dev);
}

module_init(led_dev_init);
module_exit(led_dev_exit);

MODULE_LICENSE("GPL");

Led_drv.c的程序源码:

/* 分配/设置/注册一个platform_driver */

#include <linux/module.h>
#include <linux/version.h>

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/io.h>

static int major;


static struct class *cls;
static volatile unsigned long *gpio_con;
static volatile unsigned long *gpio_dat;
static int pin;

static int led_open(struct inode *inode, struct file *file)
{
    //printk("first_drv_open\\n");
    /* 配置为输出 */
    *gpio_con &= ~(0x3<<(pin*2));
    *gpio_con |= (0x1<<(pin*2));
    return 0;    
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    int val;

    //printk("first_drv_write\\n");

    copy_from_user(&val, buf, count); //    copy_to_user();

    if (val == 1)
    {
        // 点灯
        *gpio_dat &= ~(1<<pin);
    }
    else
    {
        // 灭灯
        *gpio_dat |= (1<<pin);
    }
    
    return 0;
}


static struct file_operations led_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   led_open,     
    .write    =    led_write,       
};

static int led_probe(struct platform_device *pdev)
{
    struct resource        *res;

    /* 根据platform_device的资源进行ioremap */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    gpio_con = ioremap(res->start, res->end - res->start + 1);
    gpio_dat = gpio_con + 1;

    res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    pin = res->start;

    /* 注册字符设备驱动程序 */

    printk("led_probe, found led\\n");

    major = register_chrdev(0, "myled", &led_fops);

    cls = class_create(THIS_MODULE, "myled");

    class_device_create(cls, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
    
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    /* 卸载字符设备驱动程序 */
    /* iounmap */
    printk("led_remove, remove led\\n");

    class_device_destroy(cls, MKDEV(major, 0));
    class_destroy(cls);
    unregister_chrdev(major, "myled");
    iounmap(gpio_con);
    
    return 0;
}


struct platform_driver led_drv = {
    .probe        = led_probe,
    .remove        = led_remove,
    .driver        = {
        .name    = "myled",
    }
};


static int led_drv_init(void)
{
    platform_driver_register(&led_drv);
    return 0;
}

static void led_drv_exit(void)
{
    platform_driver_unregister(&led_drv);
}

module_init(led_drv_init);
module_exit(led_drv_exit);

MODULE_LICENSE("GPL");

将上面两个程序编译后得到Led_dev.ko和Led_drv.ko两个模块,假设先加载Led_dev.ko文件:insmod Led_drv.ko。

列出执行加载操作后的程序流程:加载后会先调用led_drv_init函数,这个函数只是调用platform_driver_register函数。

platform_driver_register(&led_drv);
    led_drv->driver.bus = &platform_bus_type;//platform_bus_type里面含有platform_match匹配函数
    led_drv->driver.probe = platform_drv_probe;
    led_drv->driver.remove = platform_drv_remove;
    driver_register(&led_drv->driver);
        bus_add_driver(&led_drv->driver);
            driver_attach(&led_drv->driver);
                bus_for_each_dev(led_drv->driver->bus, NULL, &led_drv->driver, __driver_attach);
                    while ((dev = next_device(&i)) && !error)
                    {
                             __driver_attach(dev, led_drv->driver);
                                     driver_probe_device(drv, dev);
                                         drv->bus->match(&led_dev->dev, drv);//最终调用到这个函数匹配,找到这个函数其实是platform_match函数
                                         really_probe(&led_dev->dev, drv);//匹配成功调用
                            dev->driver = drv;//匹配驱动
                                             drv->probe(dev);//调用led_drv->driver.probe函数
                    }

首先platform_driver_register函数会初始化driver.bus、driver.probe、driver.remove等变量

int platform_driver_register(struct platform_driver *drv)
{
    drv->driver.bus = &platform_bus_type;
    if (drv->probe)
        drv->driver.probe = platform_drv_probe;
    if (drv->remove)
        drv->driver.remove = platform_drv_remove;
    if (drv->shutdown)
        drv->driver.shutdown = platform_drv_shutdown;
    if (drv->suspend)
        drv->driver.suspend = platform_drv_suspend;
    if (drv->resume)
        drv->driver.resume = platform_drv_resume;
    return driver_register(&drv->driver);
}

其中platform_bus_type里含有platform_match函数,这个函数最终会被调用用来匹配Led_dev与Led_drv。

struct bus_type platform_bus_type = {
    .name        = "platform",
    .dev_attrs    = platform_dev_attrs,
    .match        = platform_match,
    .uevent        = platform_uevent,
    .suspend    = platform_suspend,
    .suspend_late    = platform_suspend_late,
    .resume_early    = platform_resume_early,
    .resume        = platform_resume,
};
static int platform_match(struct device * dev, struct device_driver * drv)
{
    struct platform_device *pdev = container_of(dev, struct platform_device, dev);

    return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0);//根据名称匹配dev与drv
}

接着看到driver_register函数,通过层层调用,最终会从dev链表搜索,通过platform_match函数找到与Led_drv匹配的Led_dev。找到后调用really_probe函数,really_probe函数先是执行dev->driver = drv;这样就将dev与drv联系起来了,接着调用led_probe函数,初始化驱动。因为这时候还没有注册Led_dev.ko所以不会匹配成功。

 

接着加载Led_dev.ko文件:insmod Led_dev.ko。列出执行加载操作后的程序流程:加载后会先调用led_dev_init函数,这个函数只是调用platform_device_register函数。

platform_device_register(&led_dev);
    platform_device_add(&led_dev)
        device_add(&led_dev->dev);
            bus_attach_device(&led_dev->dev);
                device_attach(&led_dev->dev);
                        if (dev->driver) {//如果设备的驱动程序已经存在
                        {
                            device_bind_driver( &led_dev->dev);//试着链接驱动
                        }
                    bus_for_each_drv(led_dev->dev->bus, NULL, &led_dev->dev, __device_attach);
                        while ((drv = next_driver(&i)) && !error)
                        {
                            __device_attach(drv, &led_dev->dev);
                                driver_probe_device(drv, &led_dev->dev);
                                    drv->bus->match(&led_dev->dev, drv);//最终调用到这个函数匹配,找到这个函数其实是platform_match函数
                                    really_probe(&led_dev->dev, drv);//匹配成功调用
                                        rv->bus->match(&led_dev->dev, drv);//最终调用到这个函数匹配,找到这个函数其实是platform_match函数
                                      really_probe(&led_dev->dev, drv);//匹配成功调用
                        } 

通过层层调用,最终定位到device_attach函数与Led_drv不同的是,Led_dev首先会确认自己是否已经有驱动存在,如果不存在才会从drv链表搜索,通过platform_match函数找到与Led_dev匹配的Led_drv。找到后调用really_probe函数,really_probe函数先是执行dev->driver = drv;这样就将dev与drv联系起来了,接着调用led_probe函数,初始化驱动。

static int led_probe(struct platform_device *pdev)
{
    struct resource        *res;

    /* 根据platform_device的资源进行ioremap */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    gpio_con = ioremap(res->start, res->end - res->start + 1);
    gpio_dat = gpio_con + 1;

    res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    pin = res->start;

    /* 注册字符设备驱动程序 */

    printk("led_probe, found led\\n");

    major = register_chrdev(0, "myled", &led_fops);

    cls = class_create(THIS_MODULE, "myled");

    class_device_create(cls, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
    
    return 0;
}

在led_probe中,首先会从Led_dev的led_probe中获得相应的led_resource信息来配置IO端口,接着注册字符设备,然后创建设备描述符文件。这就是平台设备驱动模型整个注册匹配的过程。

与注册过程相反rmmod Led_drv最终会调用led_remove函数

static int led_remove(struct platform_device *pdev)
{
    /* 卸载字符设备驱动程序 */
    /* iounmap */
    printk("led_remove, remove led\\n");

    class_device_destroy(cls, MKDEV(major, 0));
    class_destroy(cls);
    unregister_chrdev(major, "myled");
    iounmap(gpio_con);
    
    return 0;
}

而执行rmmod Led_dev最终会通过层层调用到led_release函数,所以led_release函数必不可少,即使它是空的函数,什么也没做

static void led_release(struct device * dev)
{
}

以上全部就是对驱动的分离分层的实现。通过平台设备驱动模型实现。

 

以上是关于Linux: 设备节点创建移除过程简析的主要内容,如果未能解决你的问题,请参考以下文章

Linux 自带的LED 灯驱动实验

Linux——Linux驱动之玩转SPI(上)Linux下SPI驱动框架简析及SPI设备驱动代码框架实现步骤

Linux驱动之平台设备驱动模型简析(驱动分离分层概念的建立)

Linux——Linux驱动之玩转SPI(上)Linux下SPI驱动框架简析及SPI设备驱动代码框架实现步骤

Linux——Linux驱动之玩转SPI(上)Linux下SPI驱动框架简析及SPI设备驱动代码框架实现步骤

linux driver ------ 字符设备驱动之“ 创建设备节点流程 ”