赞
踩
(3条消息) Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析_知秋一叶-CSDN博客
(3条消息) linux设备驱动框架_不忘初心-CSDN博客_linux设备驱动
(3条消息) linux字符驱动框架_daha1314的博客-CSDN博客_linux字符驱动框架
(3条消息) Linux驱动开发16之再论register_chrdev_region_wangdapao12138的博客-CSDN博客
29.使用register_chrdev_region()系列来注册字符设备 - 诺谦 - 博客园 (cnblogs.com)
正点原子相关资料
驱动编写注意事项:编译驱动的时候需要用到 Linux
内核源码。因此需要解压缩 Linux
内核源码并编译 Linux
内核源码。
在 Linux
内核中:
1、使用 cdev
结构体来描述字符设备;
2、通过其成员 dev_t
来定义设备号(分为主、次设备号)以确定字符设备的唯一性;
3、通过其成员 file_operations
来定义字符设备驱动提供给 VFS
的接口函数,如常见的 open()
、read()
、write()
等;
在 Linux
字符设备驱动中:
1、模块加载函数获取设备号;
register_chrdev( )
:注册字符设备(linux
版本 2.4 之前注册方式)。
register_chrdev_region( )
:静态获取设备号
alloc_chrdev_region( )
:动态获取设备号
2、通过 cdev_init( )
建立 cdev
与 file_operations
之间的连接,通过 cdev_add( )
向系统添加一个 cdev
以完成注册;
3、模块卸载函数通过 cdev_del( )
来注销 cdev
;
4、释放设备号;
unregister_chrdev( )
:注销字符设备(linux
版本 2.4 之前注销方式)。unregister_chrdev_region( )
:注销字符设备。用户空间访问该设备的程序:
1、通过 Linux
系统调用,如 open( )
、read( )
、write( )
来调用 file_operations
结构体变量中定义字符设备驱动提供给VFS的接口函数;
<include/linux/cdev.h>
struct cdev {
struct kobject kobj; // 内嵌的内核对象
struct module *owner; // 该字符设备所在的内核模块的对象指针
const struct file_operations *ops; // 该结构描述了字符设备所能实现的方法,是极为关键的一个结构体
struct list_head list; // 该结构描述了字符设备所能实现的方法,是极为关键的一个结构体
dev_t dev; // 字符设备的设备号,由主设备号和次设备号构成
unsigned int count; // 隶属于同一主设备号的次设备号的个数
};
内核给出的操作 struct cdev
结构的接口主要有以下几个:
函数 | 说明 |
---|---|
cdev_init( ) | 主要对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接 |
cdev_alloc( ) | 主要分配一个struct cdev结构**,**动态申请一个cdev内存,并做了cdev_init中所做的初始化工作(有一点差异) |
cdev_add( ) | 向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了 |
cdev_del( ) | 向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了 |
cdev_put( ) |
/**
* 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;
}
该函数主要对 struct cdev
结构体做初始化, 最重要的就是建立 cdev
和 file_operations
之间的连接:
cdev_init
作用:
1、将整个结构体清零;
2、初始化 list
成员使其指向自身;
3、初始化 kobj
成员;
4、初始化 ops
成员;
/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
该函数主要分配一个 struct cdev
结构,动态申请一个 cdev
内存,并做了 cdev_init
中所做的前面 3 步初始化工作(第四步初始化工作需要在调用 cdev_alloc
后,显式的做初始化即: .ops=xxx_ops
)。
在上面的两个初始化的函数中,我们没有看到关于 owner
成员、dev
成员、count
成员的初始化;其实,owner
成员的存在体现了驱动程序与内核模块间的亲密关系,struct module
是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE
,该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而 dev
成员和 count
成员则在 cdev_add
中才会赋上有效的值。
/** * cdev_add() - add a char device to the system * @p: the cdev structure for the device 设备的cdev结构 * @dev: the first device number for which this device is responsible 该设备负责的第一个设备号 * @count: the number of consecutive minor numbers corresponding to this 与此设备对应的连续次要号码的数目 * device * * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */ int cdev_add(struct cdev *p, dev_t dev, unsigned count) { int error; p->dev = dev; p->count = count; error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); if (error) return error; kobject_get(p->kobj.parent); return 0; }
该函数向内核注册一个 struct cdev
结构,即正式通知内核由 struct cdev *p
代表的字符设备已经可以使用了。
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
该函数向内核注销一个 struct cdev
结构,即正式通知内核由 struct cdev *p
代表的字符设备已经不可以使用了。
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
typedef unsigned int __u32; // 定义在 include/uapi/asm-generic/int-ll64.h 头文件中
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t; // 定义在 include/linux/types.h 头文件中
在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。
// include/linux/kdev_t.h #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 从设备号中提取 major #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 从设备号中提取 minor #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) // 通过 major 和 minor 构建设备号 #define print_dev_t(buffer, dev) \ sprintf((buffer), "%u:%u\n", MAJOR(dev), MINOR(dev)) #define format_dev_t(buffer, dev) \ ({ \ sprintf(buffer, "%u:%u", MAJOR(dev), MINOR(dev)); \ buffer; \ })
注:这只是构建设备号。并未注册,需要调用 register_chrdev_region
静态申请;
#define CHRDEV_MAJOR_HASH_SIZE 255
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];
内核中的每一个字符设备驱动程序都由一个 char_device_struct
结构体来描述,包含主设备号、起始次设备号、次设备号个数等信息。
内核使用 chrdevs
这个指针数组来管理所有的字符设备驱动程序,数组范围 0-255 ,看上去好像还是只支持 256 个字符设备驱动程序,其实并不然,每一个 char_device_struc
t 结构都含有一个 next
指针,它可以指向与其主设备号相同的其它字符设备驱动程序,它们之间主设备号相同,各自的次设备号范围相互不重叠。
/* 但其实这个函数是linux版本2.4之前的注册方式,它的原理是: * 1、确定一个主设备号,如果major=0,则会自动分配设备号(主设备号下所有此设备号都会被注册) * 2、构造一个file_operations结构体, 然后放在chrdevs数组中 * 3、注册 --》 * register_chrdev,cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动), * 注意这里并不是驱动文件设备节点! * 当读写字符设备的时候,就会根据主设备号从chrdevs数组中取出相应的结构体,并调用相应的处理函数 * 缺点: * 每注册一个字符设备,还会连续注册0~255个次设备号,使它们绑定在同一个 file_operations 操作方法结构体上, * 在大多数情况下,都只用极少的次设备号,所以会浪费很多资源。 */ static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); }
/** * __register_chrdev() - create and register a cdev occupying a range of minors * @major: major device number or 0 for dynamic allocation * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: name of this range of devices * @fops: file operations associated with this devices * * If @major == 0 this functions will dynamically allocate a major and return * its number. * * If @major > 0 this function will attempt to reserve a device with the given * major number and will return zero on success. * * Returns a -ve errno on failure. * * The name of this device has nothing to do with the name of the device in * /dev. It only helps to keep track of the different owners of devices. If * your module name has only one type of devices it's ok to use e.g. the name * of the module here. */ 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; }
register_chrdev
调用了 __register_chrdev_region
并强制指定了起始次设备号为0,256个,把一个主设备号下的所有次设备号都申请光了。同时它还封装了 cdev_init
和 cdev_add
。
在 2.4
的内核我们使用 register_chrdev(0, "hello", &hello_fops)
来进行字符设备设备节点的分配,这种方式每一个主设备号只能存放一种设备,它们使用相同的 file_operation
结构体,也就是说内核最多支持 256
个字符设备驱动程序。
/** * register_chrdev_region() - register a range of device numbers * @from: the first in the desired range of device numbers; must include * the major number. * @count: the number of consecutive device numbers required * @name: the name of the device or driver. * * Return value is zero on success, a negative error code on failure. * * from: 注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0 * count:需要连续注册的次设备编号个数, * 比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上 * name:字符设备名称 */ 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); 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); }
在 2.6 的内核之后,新增了一个 register_chrdev_region
函数,它支持将同一个主设备号下的次设备号进行分段,每一段供给一个字符设备驱动程序使用,使得资源利用率大大提升,同时,2.6 的内核保留了原有的 register_chrdev
方法。在 2.6 的内核中这两种方法都会调用到 __register_chrdev_region
函数。
register_chrdev_region
则是根据要求的范围进行申请,同时我们需要手动 cdev_init
、cdev_add
。
/** * alloc_chrdev_region() - register a range of char device numbers * @dev: output parameter for first assigned number * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: the name of the associated device or driver * * Allocates a range of char device numbers. The major number will be * chosen dynamically, and returned (along with the first minor number) * in @dev. Returns zero or a negative error code. * * dev :alloc_chrdev_region函数向内核申请下来的设备号 * baseminor :次设备号的起始 * count: 申请次设备号的个数 * name :执行 cat /proc/devices显示的名称 */ 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); if (IS_ERR(cd)) return PTR_ERR(cd); *dev = MKDEV(cd->major, cd->baseminor); return 0; }
让内核分配给我们一个尚未使用的主设备号,不是由我们自己指定的。
/* * Register a single major with a specified minor range. * * If major == 0 this functions will dynamically allocate a major and return * its number. * * If major > 0 this function will attempt to reserve the passed range of * minors and will return zero on success. * * Returns a -ve errno on failure. */ static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { struct char_device_struct *cd, **cp; int ret = 0; int i; cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); if (cd == NULL) return ERR_PTR(-ENOMEM); mutex_lock(&chrdevs_lock); /* temporary */ if (major == 0) { for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) { if (chrdevs[i] == NULL) break; } if (i == 0) { ret = -EBUSY; goto out; } major = i; } cd->major = major; cd->baseminor = baseminor; cd->minorct = minorct; strlcpy(cd->name, name, sizeof(cd->name)); i = major_to_index(major); for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ((*cp)->major > major || ((*cp)->major == major && (((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)))) break; /* Check for overlapping minor ranges. */ if (*cp && (*cp)->major == major) { int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1; int new_min = baseminor; int new_max = baseminor + minorct - 1; /* New driver overlaps from the left. */ if (new_max >= old_min && new_max <= old_max) { ret = -EBUSY; goto out; } /* New driver overlaps from the right. */ if (new_min <= old_max && new_min >= old_min) { ret = -EBUSY; goto out; } } cd->next = *cp; *cp = cd; mutex_unlock(&chrdevs_lock); return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }
具体分析见:(3条消息) 浅析字符设备驱动程序__register_chrdev_region_漫不经心-CSDN博客
/**
* 注销设备驱动程序的内核函数
* major:主设备号
* name:设备文件
*/
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
/** * __unregister_chrdev - unregister and destroy a cdev * @major: major device number * @baseminor: first of the range of minor numbers * @count: the number of minor numbers this cdev is occupying * @name: name of this range of devices * * Unregister and destroy the cdev occupying the region described by * @major, @baseminor and @count. This function undoes what * __register_chrdev() did. */ void __unregister_chrdev(unsigned int major, unsigned int baseminor, unsigned int count, const char *name) { struct char_device_struct *cd; cd = __unregister_chrdev_region(major, baseminor, count); if (cd && cd->cdev) cdev_del(cd->cdev); kfree(cd); }
/** * unregister_chrdev_region() - return a range of device numbers * @from: the first in the range of numbers to unregister * @count: the number of device numbers to unregister * * This function will unregister a range of @count device numbers, * starting with @from. The caller should normally be the one who * allocated those numbers in the first place... */ void unregister_chrdev_region(dev_t from, unsigned count) { 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; kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); } }
static struct char_device_struct * __unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct) { struct char_device_struct *cd = NULL, **cp; int i = major_to_index(major); mutex_lock(&chrdevs_lock); for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ((*cp)->major == major && (*cp)->baseminor == baseminor && (*cp)->minorct == minorct) break; if (*cp) { cd = *cp; *cp = cd->next; } mutex_unlock(&chrdevs_lock); return cd; }
struct file_operations { struct module *owner; //拥有该结构的模块的指针,一般为THIS_MODULES loff_t (*llseek) (struct file *, loff_t, int); //用来修改文件当前的读写位置 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //从设备中同步读取数据 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //向设备发送数据 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*mremap)(struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); // 打开 int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); // 关闭 int (*fsync) (struct file *, loff_t, loff_t, int datasync); //刷新待处理的数据 int (*aio_fsync) (struct kiocb *, int datasync); //刷新待处理的数据 int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化 int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif };
在 Linux
中一切皆为文件,驱动加载成功以后会在 “/dev”
目录下生成一个相应的文件,应用程序通过对这个名为 “/dev/xxx”
(xxx
是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
应用程序运行在用户空间,而 Linux
驱动属于内核的一部分,因此驱动运行于内核空间。
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init()
函数用来向 Linux
内核注册一个模块加载函数,参数 xxx_init
就是需要注册的具体函数,当使用 “insmod”
命令加载驱动的时候,xxx_init
这个函数就会被调用。
module_exit()
函数用来向 Linux
内核注册一个模块卸载函数,参数 xxx_exit
就是需要注册的具体函数,当使 用 “rmmod”
命令卸载具体驱动的时候 xxx_exit
函数就会被调用。
字符设备驱动模块加载和卸载模板如下所示:
/* 驱动入口函数 */ static int __init xxx_init(void) { /* 入口函数具体内容 */ return 0; } /* 驱动出口函数 */ static void __exit xxx_exit(void) { /* 出口函数具体内容 */ } // __init、__exit 定义在 inlucde/linux/init.h // 声明在 inlucde/linux/init.h /* 将上面两个函数指定为驱动的入口和出口函数 */ module_init(xxx_init); module_exit(xxx_exit);
驱动编译完成以后扩展名为 .ko
,有两种命令可以加载驱动模块:insmod
和 modprobe
,insmod
是最简单的模块加载命令,此命令用于加载指定的 .ko
模块,比如加载 drv.ko
这个驱动模块,命 令如下:
insmod drv.ko
insmod
命令不能解决模块的依赖关系,比如 drv.ko
依赖 first.ko
这个模块,就必须先使用 insmod
命令加载 first.ko
这个模块,然后再加载 drv.ko
这个模块。
但是 modprobe
就不会存在这个问题,modprobe
会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe
命令相比 insmod
要智能一些。modprobe
命令主要智能在提供了模块的依赖性分析、 错误检查、错误报告等功能,推荐使用modprobe
命令来加载驱动。modprobe
命令默认会去 /lib/modules/<kernel-version>
目录中查找模块,比如本书使用的 Linux kernel
的版本号为 4.1.15
, 因此modprobe
命令默认会到 /lib/modules/4.1.15
这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。
驱动模块的卸载使用命令 “rmmod”
即可,比如要卸载 drv.ko,使用如下命令即可:
rmmod drv.ko
也可以使用 “modprobe -r”
命令卸载驱动,比如要卸载 drv.ko,命令如下:
modprobe -r drv.ko
使用 modprobe
命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用modprobe
来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod
命令。
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev
函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major
:主设备号,Linux
下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。name
:设备名字,指向一串字符串。fops
:结构体 file_operations
类型指针,指向设备的操作函数集合变量。unregister_chrdev
函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major
:要注销的设备对应的主设备号。name
:要注销的设备对应的设备名。一般字符设备的注册在驱动模块的入口函数 xxx_init( )
中进行,字符设备的注销在驱动模块的出口函数 xxx_exit( )
中进行。模板如下:
static struct file_operations test_fops; /* 驱动入口函数 */ static int __init xxx_init(void) { /* 入口函数具体内容 */ int retvalue = 0; /* 注册字符设备驱动 */ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0){ /* 字符设备注册失败,自行处理 */ } return 0; } /* 驱动出口函数 */ static void __exit xxx_exit(void) { /* 出口函数具体内容 */ /* 注销字符设备驱动 */ unregister_chrdev(200, "chrtest"); } /* 将上面两个函数指定为驱动的入口和出口函数 */ module_init(xxx_init); module_exit(xxx_exit);
file_operations
结构体就是设备的具体操作函数,我们定义了 file_operations
结构体类型的变量 test_fops
,但是还没对其进行初始化,也就是初始化其中的 open
、 release
、read
和 write
等具体的设备操作函数。
添加基本操作,添加后内容如下:
/* 打开设备 */ static int chrtest_open(struct inode *inode, struct file *filp) { /* 用户实现具体功能 */ return 0; } /* 从设备读取 */ static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { /* 用户实现具体功能 */ return 0; } /* 向设备写数据 */ static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { /* 用户实现具体功能 */ return 0; } /* 关闭/释放设备 */ static int chrtest_release(struct inode *inode, struct file *filp) { /* 用户实现具体功能 */ return 0; } static struct file_operations test_fops = { .owner = THIS_MODULE, .open = chrtest_open, .read = chrtest_read, .write = chrtest_write, .release = chrtest_release, }; /* 驱动入口函数 */ static int __init xxx_init(void) { /* 入口函数具体内容 */ int retvalue = 0; /* 注册字符设备驱动 */ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0){ /* 字符设备注册失败,自行处理 */ } return 0; } /* 驱动出口函数 */ static void __exit xxx_exit(void) { /* 出口函数具体内容 */ /* 注销字符设备驱动 */ unregister_chrdev(200, "chrtest"); } /* 将上面两个函数指定为驱动的入口和出口函数 */ module_init(xxx_init); module_exit(xxx_exit);
我们需要在驱动中加入 LICENSE
信息和作者信息,其中 LICENSE
是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用 如下两个函数:
MODULE_LICENSE() //添加模块LICENSE信息
MODULE_AUTHOR() //添加模块作者信息
添加后内容如下:
……
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
// 声明在 inlucde/linux/module.h
/* LICENSE 采用GPL 协议 */
MODULE_LICENSE("GPL");
我们以 chrdevbase
这个虚拟设备为例,完整的编写一个字符设备驱动模块。chrdevbase
不是实际存在的一个设备,是为了方便说明字符设备的开发而引入的一个虚拟设备。
在 Ubuntu
中创建一个目录用来存放 Linux
驱动程序,比如我创建了一个名为 linux_driver
的目录来存放所有的 Linux
驱动。
onlylove@ubuntu:~/linux/driver/linux_driver$ pwd
/home/onlylove/linux/driver/linux_driver
在 linux_driver
目录下新建一个名为 1_chrdevbase
的子目录来存放本实验所有文件。
onlylove@ubuntu:~/linux/driver/linux_driver$ mkdir 1_chrdevbase
onlylove@ubuntu:~/linux/driver/linux_driver$ ls
1_chrdevbase
onlylove@ubuntu:~/linux/driver/linux_driver$
在 1_chrdevbase
目录中新建 VSCode
工程:
1、保存工程文件 1_chrdevbase.code-workspace
。
2、新建 chrdevbase.c
文件
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ pwd
/home/onlylove/linux/driver/linux_driver/1_chrdevbase
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls -al
total 12
drwxrwxr-x 2 onlylove onlylove 4096 Nov 29 04:31 .
drwxrwxr-x 3 onlylove onlylove 4096 Nov 29 04:26 ..
-rw-rw-r-- 1 onlylove onlylove 60 Nov 29 04:31 1_chrdevbase.code-workspace
-rw-rw-r-- 1 onlylove onlylove 0 Nov 29 04:29 chrdevbase.c
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$
因为是编写 Linux
驱动,因此会用到 Linux
源码中的函数。我们需要在 VSCode
中添加 Linux
源码中的头文件路径。打开 VSCode
,按下 “Crtl+Shift+P”
打开 VSCode
的控制台,然后输入 “C/C++: Edit configurations(JSON) ”
,打开 C/C++
编辑配置文件,如图所示:
打开以后会自动在 .vscode
目录下生成一个名为 c_cpp_properties.json
的文件,此文件默认内容如下所示:
{ "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**" ], "defines": [], "compilerPath": "/usr/bin/gcc", "cStandard": "gnu17", "cppStandard": "gnu++14", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 }
includePath
表示头文件路径,需要将 Linux
源码里面的头文件路径添加进来,也就是我们前面移植的 Linux
源码中的头文件路径。
需要添加路径:
1、linux/include
。
2、linux//arch/arm/include
。
3、linux/arch/arm/include/gener ated
。
{ "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**", "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include", "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include", "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated" ], "defines": [], "compilerPath": "/usr/bin/gcc", "cStandard": "gnu17", "cppStandard": "gnu++14", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 }
注意,这里使用了绝对路径。
1、指定内核路径,使用 KERNELDIR
变量。
2、指定输入文件,使用 CURRENT_PATH
变量。
3、编译为模块,使用 obj-m
说明。
4、将当前工作目录转移到你所指定的位置,使用 -C
选项。
5、当用户需要以某个内核为基础编译一个外部模块的话,使用 M
选项。
KERNELDIR := /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
#include "linux/init.h" #include "linux/module.h" /* 驱动入口函数 */ static int __init chrdevbase_init(void) { /* 入口函数具体内容 */ return 0; } /* 驱动出口函数 */ static void __exit chrdevbase_exit(void) { /* 出口函数具体内容 */ } // 声明在 linux/init.h 头文件中 /* 将上面两个函数指定为驱动的入口和出口函数 */ module_init(chrdevbase_init); module_exit(chrdevbase_exit); // 声明在 linux/module.h 头文件中 MODULE_LICENSE("GPL");
make
编译过程如下:
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ make
make -C /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/home/onlylove/linux/driver/linux_driver/1_chrdevbase modules
make[1]: Entering directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
CC [M] /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.mod.o
LD [M] /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.ko
make[1]: Leaving directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$
加载 .ko
文件,测试是否成功,测试过程如下:
/ # ls
bin etc mnt sbin usr
chrdevbase.ko lib proc sys
dev linuxrc root tmp
/ # lsmod
Module Size Used by Not tainted
/ # insmod chrdevbase.ko
/ # lsmod
Module Size Used by Tainted: G
chrdevbase 586 0
/ # rmmod chrdevbase.ko
/ # lsmod
Module Size Used by Tainted: G
/ #
#include "linux/init.h" #include "linux/module.h" #include "linux/fs.h" // 声明在 linux/fs.h 头文件中 static struct file_operations fops; /* 驱动入口函数 */ static int __init chrdevbase_init(void) { /* 入口函数具体内容 */ int retvalue = 0; // 声明在 linux/fs.h 头文件中 retvalue = register_chrdev(200,"chrdev",&fops); if(retvalue < 0){ /* 字符设备注册失败 */ } return 0; } /* 驱动出口函数 */ static void __exit chrdevbase_exit(void) { /* 出口函数具体内容 */ // 声明在 linux/fs.h 头文件中 unregister_chrdev(200,"chrdev"); } // 声明在 linux/init.h 头文件中 /* 将上面两个函数指定为驱动的入口和出口函数 */ module_init(chrdevbase_init); module_exit(chrdevbase_exit); // 声明在 linux/module.h 头文件中 MODULE_LICENSE("GPL");
加载 .ko
文件,测试是否成功,测试过程如下:
/ # ls bin etc mnt sbin usr chrdevbase.ko lib proc sys dev linuxrc root tmp / # lsmod Module Size Used by Tainted: G / # 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 13 input 29 fb 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 180 usb 189 usb_device 207 ttymxc 216 rfcomm 226 drm 249 ttyLP 250 iio 251 watchdog 252 ptp 253 pps 254 rtc Block devices: 1 ramdisk 259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd 128 sd 129 sd 130 sd 131 sd 132 sd 133 sd 134 sd 135 sd 179 mmc / # / # insmod chrdevbase.ko / # lsmod Module Size Used by Tainted: G chrdevbase 832 0 / # 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 13 input 29 fb 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 180 usb 189 usb_device 200 chrdev 207 ttymxc 216 rfcomm 226 drm 249 ttyLP 250 iio 251 watchdog 252 ptp 253 pps 254 rtc Block devices: 1 ramdisk 259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd 128 sd 129 sd 130 sd 131 sd 132 sd 133 sd 134 sd 135 sd 179 mmc / # rmmod chrdevbase.ko / # lsmod Module Size Used by Tainted: G / # 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 13 input 29 fb 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 180 usb 189 usb_device 207 ttymxc 216 rfcomm 226 drm 249 ttyLP 250 iio 251 watchdog 252 ptp 253 pps 254 rtc Block devices: 1 ramdisk 259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd 128 sd 129 sd 130 sd 131 sd 132 sd 133 sd 134 sd 135 sd 179 mmc / #
通过以上日志可以看到,使用 insmod
加载模块后,/proc/devices
下注册 200 chrdev
。使用 rmmod
卸载模块后,/proc/devices
下销毁 200 chrdev
。
具体操作就是使用自定义函数,将函数指针赋值到 file_operations
结构体中。
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*mremap)(struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif };
核心:对 file_operations
结构体变量进行赋值。
#include "linux/init.h" #include "linux/module.h" #include "linux/fs.h" #include "linux/types.h" // struct inode 声明在 linux/fs.h 中 // struct file 声明在 linux/fs.h 中 int chrdev_open (struct inode *i, struct file *f) { // printk 声明在 linux/printk.h 中 printk("chrdevbase open!\r\n"); return 0; } int chrdev_release (struct inode *i, struct file *f) { printk("chrdevbase release!\r\n"); return 0; } // ssize_t 定义在 linux/types.h 中 // __user 定义在 linux/compiler.h 中 // size_t 定义在 linux/types.h 中 // loff_t 定义在 linux/types.h 中 ssize_t chrdev_read (struct file *f, char __user *b, size_t c, loff_t * l) { printk("chrdevbase read!\r\n"); return 0; } ssize_t chrdev_write (struct file *f, const char __user *b, size_t c, loff_t *l) { printk("chrdevbase write!\r\n"); return 0; } // 声明在 linux/fs.h 头文件中 static struct file_operations fops = { .open = chrdev_open, .release = chrdev_release, .read = chrdev_read, .write = chrdev_write, }; /* 驱动入口函数 */ static int __init chrdevbase_init(void) { /* 入口函数具体内容 */ int retvalue = 0; // 声明在 linux/fs.h 头文件中 retvalue = register_chrdev(200,"chrdev",&fops); if(retvalue < 0){ /* 字符设备注册失败 */ } return 0; } /* 驱动出口函数 */ static void __exit chrdevbase_exit(void) { /* 出口函数具体内容 */ // 声明在 linux/fs.h 头文件中 unregister_chrdev(200,"chrdev"); } // 声明在 linux/init.h 头文件中 /* 将上面两个函数指定为驱动的入口和出口函数 */ module_init(chrdevbase_init); module_exit(chrdevbase_exit); // 声明在 linux/module.h 头文件中 MODULE_LICENSE("GPL");
特别说明:
这里使用了 printk
来输出信息, 而不是 printf
!因为在 Linux
内核中没有 printf
这个函数。printk
相当于 printf
的孪生兄妹,printf
运行在用户态,printk
运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用 printk
这个函数。不同之处在于,printk
可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件 include/linux/kern_levels.h
里面,定义如下:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* system is unusable 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* critical conditions 临界条件,比如严重的软件或硬件错误 */
#define KERN_ERR KERN_SOH "3" /* error conditions 错误状态,一般设备驱动程序中使用 KERN_ERR报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* warning conditions 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* informational 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages 调试信息 */
一共定义了 8 个级别,其中 0 的优先级最高,7 的优先级最低。如果要设置消息级别,参考如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
具体操作函数需要通过应用层函数调用完成测试。
1、编译驱动模块
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace chrdevbase_app.c chrdevbase.c Makefile
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ make
make -C /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/home/onlylove/linux/driver/linux_driver/1_chrdevbase modules
make[1]: Entering directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
CC [M] /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.mod.o
LD [M] /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.ko
make[1]: Leaving directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace chrdevbase.c chrdevbase.mod.c chrdevbase.o modules.order
chrdevbase_app.c chrdevbase.ko chrdevbase.mod.o Makefile Module.symvers
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ cp chrdevbase.ko /home/onlylove/linux/nfs/rootfs-l/chrdevbase.ko
2、编写 app
程序
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "stdio.h" int main(int argc, char *argv[]) { int fd = 0, retvalue = 0; char readbuf[100] = "", writebuf[100] = ""; fd = open(argv[1],O_RDWR); if(fd < 0){ printf("Can't open file %s\r\n", argv[1]); return -1; } read(fd, readbuf, 50); write(fd, writebuf, 50); retvalue = close(fd); if(retvalue < 0){ printf("Can't close file %s\r\n", argv[1]); return -1; } return 0; }
3、编译 app
程序
arm-linux-gnueabihf-gcc chrdevbase_app.c -o chrdevbase_app
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace chrdevbase.c chrdevbase.mod.c chrdevbase.o modules.order
chrdevbase_app.c chrdevbase.ko chrdevbase.mod.o Makefile Module.symvers
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ arm-linux-gnueabihf-gcc chrdevbase_app.c -o chrdevbase_app
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace chrdevbase_app.c chrdevbase.ko chrdevbase.mod.o Makefile Module.symvers
chrdevbase_app chrdevbase.c chrdevbase.mod.c chrdevbase.o modules.order
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ cp chrdevbase_app /home/onlylove/linux/nfs/rootfs-l/chrdevbase_app
/ # ls bin etc proc tmp chrdevbase.ko lib root usr chrdevbase_app linuxrc sbin dev mnt sys / # lsmod Module Size Used by Tainted: G / # insmod chrdevbase.ko / # lsmod Module Size Used by Tainted: G chrdevbase 1282 0 / # 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 13 input 29 fb 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 180 usb 189 usb_device 200 chrdev 207 ttymxc 216 rfcomm 226 drm 249 ttyLP 250 iio 251 watchdog 252 ptp 253 pps 254 rtc Block devices: 1 ramdisk 259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd 128 sd 129 sd 130 sd 131 sd 132 sd 133 sd 134 sd 135 sd 179 mmc / #
驱动加载成功需要在 /dev
目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建 /dev/chrdevbase
这个设备节 点文件:
mknod /dev/chrdevbase c 200 0
其中 “mknod”
是创建节点命令,“/dev/chrdevbase”
是要创建的节点文件,“c”
表示这是个字符设备,“200”
是设备的主设备号,“0”
是设备的次设备号。创建完成以后就会存在 /dev/chrdevbase
这个文件,可以使用 “ls /dev/chrdevbase -l”
命令查看,结果下所示:
/dev # ls /dev/chrdevbase -l
crw-r--r-- 1 0 0 200, 0 Jan 1 01:38 /dev/chrdevbase
测试过程如下:
/dev # ls /dev/chrdevbase -l
ls: /dev/chrdevbase: No such file or directory
/dev # mknod /dev/chrdevbase c 200 0
/dev # ls /dev/chrdevbase -l
crw-r--r-- 1 0 0 200, 0 Jan 1 01:38 /dev/chrdevbase
/dev #
如果 chrdevbase_app
想要读写 chrdevbase
设备,直接对 /dev/chrdevbase
进行读写操作即可。相当于 /dev/chrdevbase
这个文件是 chrdevbase
设备在用户空间中的实现。
使用 chrdevbase_app 软件操作 chrdevbase 这个设备,看看读写是否正常,首先进行读操作,输入如下命令:
./chrdevbase_app /dev/chrdevbase
测试结果如下:
/ # ls
bin etc proc tmp
chrdevbase.ko lib root usr
chrdevbase_app linuxrc sbin
dev mnt sys
/ # ./chrdevbase_app /dev/chrdevbase
chrdevbase open!
chrdevbase read!
chrdevbase write!
chrdevbase release!
/ #
如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉 chrdevbase
这个设备:
rmmod chrdevbase.ko
卸载以后使用 lsmod
命令查看 chrdevbase
这个模块还存不存在。
测试结果如下:
/ # lsmod
Module Size Used by Tainted: G
chrdevbase 1282 0
/ # rmmod chrdevbase.ko
/ # lsmod
Module Size Used by Tainted: G
/ # ls /dev/chrdevbase -l
ls: /dev/chrdevbase: No such file or directory
/ #
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。