1. 驱动开发 字符设备驱动
代码:
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 1 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称
/***** 字符设备注册步骤 ***************************************************
* 初始化函数
* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)
* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或
* 注销函数
* 1.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数)
*
* 跟文件系统下创建 设备文件 cat /proc/devices
* 查看到设备信息
Character devices:
1 mem
256 vser 我们自己申请的设备信息
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
* ***********************************************************************/
static int __init vser_init(void)
{
int ret; //用于获取函数返回值,并该函数是否执行成功
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
ret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("静态申请设备号失败\n");
ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("动态申请设备号失败\n");
goto register_err;
}
VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号
VSER_MINOR = MINOR(dev); //从设备号中提取次设备号
}
return 0; //表示成功,直接结束函数并返回0
register_err: /* 设备号申请失败 */
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
unregister_chrdev_region(dev,VSER_DEV_CNT);
}
/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
Makefile
#动态编译内核驱动生成.ko文件的Makeifle
#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件
#内核源代码路径
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/student/linux-5.4.31
else
KERNELDIR ?= /lib/modules/${shell uname -r}/build
endif
#当前模块路径
PWD ?= $(shell pwd)
#编译源码生成 .ko 文件 make all
all:
${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:
rm Module.* modules.* *.mod *.ko
1. 命令: make (编译形成vser .ko 文件)
2. 命令 : mknod /dev/vser0 c 256 0 (创建一个字符设备)
解释
mknod == make node //创建一个节点
/dev/vser0 设备名称
c 字符设备
256 主设备号
0 次设备号
3. 命令 : ls -l /dev/vser0(设备查看存在)
4.命令: sudo rmmod vser (卸载模块 防止之前的模块没有卸载)
5. 命令 : sudo insmod vser.ko (加载 模块)
6. 命令 : lsmod ( 查看模块是否加载)
7. 命令: dmesg (查看 内核模块的输出 比如说 printk)(这个没有内核信息打印)
8. 过程多差不多!!!! (下面的 我将不写过程了,,除了一个驱动 多个设备)
2. 字符设备注册
代码:
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 1 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称
/**** 字符设备结构体 ******/
static struct cdev vser_cdev; //字符设备结构体
/**** 设备操作相关函数 到时候就可以使用 open 打开该设备文件 *****/
int vser_open(struct inode *p_inode, struct file *p_file)
{
printk("vser设备打开了\n");
return 0;
}
/**** 设备操作集 *****/
static struct file_operations vser_ops = { //设备操作集结构体
.owner = THIS_MODULE,
.open = vser_open,
};
/***** 字符设备注册步骤 ***************************************************
* 初始化函数
* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)
* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或
* 3.初始化字符设备 cdev_init(字符设备结构体地址,操作集结构体地址)
* 4.将字符设备添加到内核散列表map中 cdev_add(字符设备结构体地址,设备号,设备个数)
* 注销函数
* 1.从内核散链表map中删除字符设备 cdev_del(字符设备结构体地址)
* 2.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数)
*
* 跟文件系统下创建 设备文件 cat /proc/devices
* 查看到设备信息
Character devices:
1 mem
256 vser 我们自己申请的设备信息
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
* ***********************************************************************/
static int __init vser_init(void)
{
int ret; //用于获取函数返回值,并该函数是否执行成功
/********* 申请设备号 **************/
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
ret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("静态申请设备号失败\n");
ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("动态申请设备号失败\n");
goto register_err;
}
VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号
VSER_MINOR = MINOR(dev); //从设备号中提取次设备号
}
/******** 申请字符设备 *********/
cdev_init(&vser_cdev,&vser_ops);
vser_cdev.owner = THIS_MODULE;
ret = cdev_add(&vser_cdev,dev,VSER_DEV_CNT);
if(ret != 0)
{
printk("申请字符设备失败\n");
goto cdev_err;
}
return 0; //表示成功,直接结束函数并返回0
cdev_err: /* 申请字符设备失败 */
unregister_chrdev_region(dev,VSER_DEV_CNT); //释放设备号
register_err: /* 设备号申请失败 */
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
cdev_del(&vser_cdev); //1.从内核散列表中删除字符设备
unregister_chrdev_region(dev,VSER_DEV_CNT); //2.释放设备号
}
/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
main.c (记得使用 gcc 编译 )
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
if(argc != 2)
{
return -1;
}
int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数
if(fd < 0)
{
perror("打开设备失败:");
return -2;
}
close(fd);
return 0;
}
Makefile
#动态编译内核驱动生成.ko文件的Makeifle
#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件
#内核源代码路径
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/student/linux-5.4.31
else
KERNELDIR ?= /lib/modules/${shell uname -r}/build
endif
#当前模块路径
PWD ?= $(shell pwd)
#编译源码生成 .ko 文件 make all
all:
${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:
rm Module.* modules.* *.mod *.ko
1. 过程: gcc mian.c (把main.c 编译成 a.out)
2. sudo rmmod vser (卸载之前加载的模块)
3. make (编译 makefile 文件)
4. sudo insmod vser.ko (加载 模块)
5.命令: lsmod (查看是否加载)
6. dmesg (查看内核打印的信息, 这个没有内核信息打印)
3.虚拟串口
代码:
main_read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{
if(argc != 2)
{
return -1;
}
int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数
if(fd < 0)
{
perror("打开设备失败:");
return -2;
}
char buf[10];
while(1)
{
memset(buf,0,sizeof(buf));
read(fd,buf,10);
printf("读取到:%s\n",buf);
sleep(1);
}
close(fd);
return 0;
}
main_write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{
if(argc != 2)
{
return -1;
}
int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数
if(fd < 0)
{
perror("打开设备失败:");
return -2;
}
char buf[10];
while(1)
{
printf("请输入要写入的数据:");
scanf("%s",buf);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 1 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称
/**** 字符设备结构体 ******/
static struct cdev vser_cdev; //字符设备结构体
/**** 虚拟串口 ******/
#include <linux/kfifo.h>
DEFINE_KFIFO(vser_fifo,char,32); //创建一个名为 vser_fifo 的 char 类型 大小为 32 个的队列管道
/**** 设备操作相关函数 到时候就可以使用 open 打开该设备文件 *****/
int vser_open(struct inode *p_inode, struct file *p_file)
{
printk("vser设备打开了\n");
return 0;
}
int vser_close(struct inode *p_inode, struct file *p_file)
{
printk("vser设备关闭了\n");
return 0;
}
ssize_t vser_read(struct file *p_file, char __user *user_buf, size_t user_size, loff_t *ps)
{
int copied = 0;
kfifo_to_user(&vser_fifo,user_buf,user_size,&copied); //将内核队列管道数据拷贝到用户空间
return copied;
}
ssize_t vser_write(struct file *p_file, const char __user *user_buf, size_t user_size, loff_t *ps)
{
int copied = 0;
kfifo_from_user(&vser_fifo,user_buf,user_size,&copied);//将用户空间数据拷贝到内核队列管道中
return copied;
}
/**** 设备操作集 *****/
static struct file_operations vser_ops = { //设备操作集结构体
.owner = THIS_MODULE,
.release = vser_close,
.open = vser_open,
.read = vser_read,
.write = vser_write,
};
/***** 字符设备注册步骤 ***************************************************
* 初始化函数
* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)
* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或
* 3.初始化字符设备 cdev_init(字符设备结构体地址,操作集结构体地址)
* 4.将字符设备添加到内核散列表map中 cdev_add(字符设备结构体地址,设备号,设备个数)
* 注销函数
* 1.从内核散链表map中删除字符设备 cdev_del(字符设备结构体地址)
* 2.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数)
*
* 跟文件系统下创建 设备文件 cat /proc/devices
* 查看到设备信息
Character devices:
1 mem
256 vser 我们自己申请的设备信息
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
* ***********************************************************************/
static int __init vser_init(void)
{
int ret; //用于获取函数返回值,并该函数是否执行成功
/********* 申请设备号 **************/
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
ret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("静态申请设备号失败\n");
ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("动态申请设备号失败\n");
goto register_err;
}
VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号
VSER_MINOR = MINOR(dev); //从设备号中提取次设备号
}
/******** 申请字符设备 *********/
cdev_init(&vser_cdev,&vser_ops);
vser_cdev.owner = THIS_MODULE;
ret = cdev_add(&vser_cdev,dev,VSER_DEV_CNT);
if(ret != 0)
{
printk("申请字符设备失败\n");
goto cdev_err;
}
return 0; //表示成功,直接结束函数并返回0
cdev_err: /* 申请字符设备失败 */
unregister_chrdev_region(dev,VSER_DEV_CNT); //释放设备号
register_err: /* 设备号申请失败 */
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
cdev_del(&vser_cdev); //1.从内核散列表中删除字符设备
unregister_chrdev_region(dev,VSER_DEV_CNT); //2.释放设备号
}
/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
Makefile
#动态编译内核驱动生成.ko文件的Makeifle
#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件
#内核源代码路径
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/student/linux-5.4.31
else
KERNELDIR ?= /lib/modules/${shell uname -r}/build
endif
#当前模块路径
PWD ?= $(shell pwd)
#编译源码生成 .ko 文件 make all
all:
${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:
rm Module.* modules.* *.mod *.ko
1. 编译所有的 .c 文件
gcc main_write.c -o write (写文件)
gcc main_read.c -o read (读文件)
make (编译 vser.ko 文件)
2. 命令: sudo rmmod vser (卸载之前加载的模块)
3. 命令: sudo vser.ko (加载现在的模块)
4. 命令: lsmod (查看模块是否加载)
5. 命令: sudo ./read /dev/vser0 (运行读文件 后面的/dev/vser0 是之前创建的字符设备)
sudo ./write /dev/vser0 (运行写文件 后面的/dev/vser0 是之前创建的字符设备)
6. 命令 : dmesg (查看内核信息)
4. 一个驱动支持多个设备
代码:
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 2 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称
// mknod /dev/vser0 c 256 0
// mknod /dev/vser1 c 256 1
/**** 字符设备结构体 ******/
static struct cdev vser_cdev; //字符设备结构体
/**** 虚拟串口 ******/
#include <linux/kfifo.h>
DEFINE_KFIFO(vser_fifo0,char,32); //创建一个名为 vser_fifo0 的 char 类型 大小为 32 个的队列管道
DEFINE_KFIFO(vser_fifo1,char,32); //创建一个名为 vser_fifo1 的 char 类型 大小为 32 个的队列管道
/**** 设备操作相关函数 到时候就可以使用 open 打开该设备文件 *****/
int vser_open(struct inode *p_inode, struct file *p_file)
{
int minor = MINOR(p_inode->i_rdev); //获取设备的次设备号
printk("vser设备打开了\n");
printk("当前的次设备号;%d\n",minor);
switch (minor)
{
case 0:
p_file->private_data = &vser_fifo0;
break;
case 1:
p_file->private_data = &vser_fifo1;
break;
}
printk("操作的FIFO = %p\n",p_file->private_data);
return 0;
}
int vser_close(struct inode *p_inode, struct file *p_file)
{
printk("vser设备关闭了\n");
return 0;
}
ssize_t vser_read(struct file *p_file, char __user *user_buf, size_t user_size, loff_t *ps)
{
int copied = 0;
struct kfifo *fifo = p_file->private_data;
kfifo_to_user(fifo,user_buf,user_size,&copied); //将内核队列管道数据拷贝到用户空间
return copied;
}
ssize_t vser_write(struct file *p_file, const char __user *user_buf, size_t user_size, loff_t *ps)
{
int copied = 0;
struct kfifo *fifo = p_file->private_data;
printk("操作的FIFO = %p\n",p_file->private_data);
kfifo_from_user(fifo,user_buf,user_size,&copied);//将用户空间数据拷贝到内核队列管道中
return copied;
}
/**** 设备操作集 *****/
static struct file_operations vser_ops = { //设备操作集结构体
.owner = THIS_MODULE,
.release = vser_close,
.open = vser_open,
.read = vser_read,
.write = vser_write,
};
/***** 字符设备注册步骤 ***************************************************
* 初始化函数
* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)
* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或
* 3.初始化字符设备 cdev_init(字符设备结构体地址,操作集结构体地址)
* 4.将字符设备添加到内核散列表map中 cdev_add(字符设备结构体地址,设备号,设备个数)
* 注销函数
* 1.从内核散链表map中删除字符设备 cdev_del(字符设备结构体地址)
* 2.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数)
*
* 跟文件系统下创建 设备文件 cat /proc/devices
* 查看到设备信息
Character devices:
1 mem
256 vser 我们自己申请的设备信息
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
* ***********************************************************************/
static int __init vser_init(void)
{
int ret; //用于获取函数返回值,并该函数是否执行成功
/********* 申请设备号 **************/
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
ret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("静态申请设备号失败\n");
ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);
if(ret != 0)
{
printk("动态申请设备号失败\n");
goto register_err;
}
VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号
VSER_MINOR = MINOR(dev); //从设备号中提取次设备号
}
/******** 申请字符设备 *********/
cdev_init(&vser_cdev,&vser_ops);
vser_cdev.owner = THIS_MODULE;
ret = cdev_add(&vser_cdev,dev,VSER_DEV_CNT);
if(ret != 0)
{
printk("申请字符设备失败\n");
goto cdev_err;
}
return 0; //表示成功,直接结束函数并返回0
cdev_err: /* 申请字符设备失败 */
unregister_chrdev_region(dev,VSER_DEV_CNT); //释放设备号
register_err: /* 设备号申请失败 */
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev; //用于存储设备号
dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINOR
cdev_del(&vser_cdev); //1.从内核散列表中删除字符设备
unregister_chrdev_region(dev,VSER_DEV_CNT); //2.释放设备号
}
/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
main_write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{
if(argc != 2)
{
return -1;
}
int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数
if(fd < 0)
{
perror("打开设备失败:");
return -2;
}
char buf[10];
while(1)
{
printf("请输入要写入的数据:");
scanf("%s",buf);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
main_read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{
if(argc != 2)
{
return -1;
}
int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数
if(fd < 0)
{
perror("打开设备失败:");
return -2;
}
char buf[10];
while(1)
{
memset(buf,0,sizeof(buf));
read(fd,buf,10);
printf("读取到:%s\n",buf);
sleep(1);
}
close(fd);
return 0;
}
Makefile
#动态编译内核驱动生成.ko文件的Makeifle
#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件
#内核源代码路径
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/student/linux-5.4.31
else
KERNELDIR ?= /lib/modules/${shell uname -r}/build
endif
#当前模块路径
PWD ?= $(shell pwd)
#编译源码生成 .ko 文件 make all
all:
${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:
rm Module.* modules.* *.mod *.ko
1. 命令: sudo /dev/vser* (卸载之前创建的字符设备模块 )
2. sudo mknod /dev/vser1 c 256 0 ( 创建字符设备)
sudo mknod /dev/vser2 c 256 1 (设备名称不同, 次设备号不同 , 自己看 vser.c 里面的 switch 的数字 决定次设备号)
3.命令: su (进入超级用户)
4. 命令:sudo rmmod vser
5.命令 : make
6. 命令: sudo insmod vser.ko
7. 命令: gcc main_read.c -o read
8. 命令: gcc main_write.c -o write
9. 命令: echo "我是" > /dev/vser1
10 . 命令: cat /dev/vser1
作业:
1.字符设备和块设备的区别不包括()
[A]字符设备按字节流进行访问,块设备按块大小进行访问
[B]字符设备只能处理可打印字符,块设备可以处理二进制数据
[C]多数字符设备不能随机访问,而块设备一定能随机访问
[D]字符设备通常没有页高速缓存,而块设备有
2.在3.14.25版本的内核中,主设备号占———位,次设备号占———位。
[A]8
[B] 16
[C] 12
[D]20
3.用于分配主次设备号的函数是()。
[A] register_chrdev_region
[B] MKDEV
[C]alloc_chrdev_region
[D] MAJOR
4、在字符设备驱动中,struct file_operations 结构中的函数指针成员不包含$,
[A] open
[B] close
[C] read
[D] show_fdinfo