0. 前言
上一篇我们讲了字符设备用户态与内核态的数据交互,这篇文章我们继续深入学习。
在学习 Linux 驱动开发过程中,大家对于 open,read,write 这些基础操作肯定是手拿把掐,但是一遇到 ioctl 和并发控制,面对一堆宏定义和死锁问题,往往会学的一头雾水。
今天,我们会继续延续使用上一篇内核态与用户态数据交互的驱动代码,用 200 多行的完整实战代码,把 ioctl 的原理及使用方法和 mutex 互斥锁的避坑指南讲得明明白白。
如果你看过我《Linux驱动开发》专栏中的上一篇文章,并认真看过代码,手敲过一遍,那么在看完本篇文章之后,你只需略做修改,即可跑通,当然,看完本篇文章你会具有修改代码的能力的。
不管你是初学者还是想复习巩固有一定基础的高手,我相信看完之后会有新的感悟的,建议收藏一下,以后复习用。
注:文末附完整可运行代码。
1. ioctl与mutex的必要性
可能还有不少读者听过这两个概念,但不知道他们是用来干什么的,我们第一章先来了解一下,如果没有它们会产生什么严重的后果,用了它们,事情又是怎样变的不一样了。
1.1 深入解析ioctl
在 Linux 中,一切皆文件。我们一般情况下都是通过 open、read、write 和 close 四个标准的系统调用来与文件或设备进行交互。
但是,并不是所有的设备交互方式都能被简单地抽象为读和写。
比如:如果你想 修改串口波特率 ,控制 LED 亮灭 ,或者我们本文的 清空缓冲区操作 ,这些操作既不是读数据,也不是写数据,而是控制指令。
如果为每一种设备的每一种控制功能都设计一个专门的系统调用,内核的接口将会爆炸式增长并且极难维护。因此, ioctl 这个系统调用出现了,它专门用于处理所有无法被标准的读写操作覆盖的设备特定操作。
1.1.1 用户态中的ioctl
在编写应用程序时,ioctl 的接口定义在 <sys/ioctl.h> 中,函数原型如下:
c
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
- 第一个参数是文件描述符
fd,这不用多说。 - 第二个参数
request是一个命令码,从他的数据类型就能看出来它是一个 无符号长整形 的数,用来告诉内核驱动程序我们要执行什么操作,request通常由设备驱动以 宏 的形式提供。 - 后面是可变参数,名为
arg,这个参数传什么取决于request,可以什么都不传,可以传一个整数,也可以传一个指针。 - 执行成功返回 0,失败返回 -1。
1.1.2 命令码详解
request 表面上是一个无符号长整数,但在 Linux 中,它并不是随便定义的。为了防止不同设备的命令码发生冲突,Linux 规定了命令码的 32 位要 按特定规则划分,携带4个信息,详细内容请看下图:

- 方向:也就是数据传输方向,表明数据是传给内核,还是从内核读取,又或者是无数据传输。
- 数据大小:传递的结构体或者变量大小,以字节为单位。
- 幻数:通常是一个 ASCII 字符(如我们将要使用的
'k'),用来唯一标识某个特定的驱动程序,防止你的命令发给了错误的设备。 - 序号:当前驱动程序内部操作的序号。
上面这四个信息,都不用我们自己填写,因为内核提供了一套 宏,用于标准化地生成命令码,像这样的宏一共有四个:
_IO(magic, seq):无数据传输。_IOR(magic, seq, type):从设备读取数据到用户空间,后面的R代表读。_IOW(magic, seq, type):向设备写入数据,W代表写。_IOWR(magic, seq, type):既向设备写入数据,又从设备读取数据,WR代表读写双向。
其中 magic 是幻数,seq 是序号,type 是数据类型。
1.1.3 驱动程序中的实现
老话说得好:光说不练假把式。了解到这个地步,我们就该看看驱动程序中具体怎么操作了。
驱动代码中,我们定义了下面命令:
c
#define MYDEV_MAGIC 'k' //幻数,随便选个字符都行,这里选的'k'
#define MYDEV_IOC_RESET _IO(MYDEV_MAGIC,0) //清空缓冲区操作
#define MYDEV_IOC_GET_SIZE _IOR(MYDEV_MAGIC, 1, int) //获取数据大小操作
- 第一个宏:我们先定义好 幻数,后面传进去,标准步骤,没什么好讲的。
- 第二个宏:定义 清空缓冲区 操作,这个操作不需要进行数据传输,因此使用
_IO(magic, seq)这个宏,后面函数中会具体实现清空缓冲区的操作。 - 第三个宏:定义 获取数据大小 的操作,这个操作我们需要从内核中读取内容,因此使用
_IOR(magic, seq, type)这个宏。
这里,我们只用了两个宏,其他两个宏相信大家也能类比推理出它们的用法。
此外,在 unlocked_ioctl 回调函数中,可以使用 _IOC_TYPE(cmd) 和 _IOC_NR(cmd) 提取出 幻数 和 序列号,进行合法性校验,具体操作如下:
c
//检查命令是否属于我们的设备
if(_IOC_TYPE(cmd) != MYDEV_MAGIC)
{
return -ENOTTY;
}
//检查序列号是否有效
if(_IOC_NR(cmd) > 1)
{
return -ENOTTY;
}
cmd 是内核根据用户程序的操作传入 unlocked_ioctl 回调函数的,这里有两个合法性检查,分别是检查设备类型和检查序列号:
_IOC_TYPE(cmd)会提取出该命令对应的幻数,将这个幻数与我们定义的幻数比较,如果不相同说明发生了错误。_IOC_NR(cmd)会提取出该命令对应的序列号。前面我忘了讲了,我们在宏定义时,为了保证 每个操作对应的序列号不同,因此,我们将序列号从 0 开始,每个命令递增 1。由于我们只有两个操作,所以序列号最大就到 1,如果提取出的序列号大于 1,那就说明发生了错误。
关于 ioctl,还有最后一点要讲。
当应用层发起调用 ioctl(fd, MYDEV_IOC_GET_SIZE, &size) 时,&size 的指针会被转化为 unsigned long arg 传递到内核态我们定义的 my_cdev_ioctl 函数中,我们要特别注意:内核绝对不能直接解引用用户态空间的指针,必须使用 copy_to_user 或 copy_from_user 进行安全拷贝:
c
//将内核中的size变量,拷贝到用户空间arg指向的内存中
if(copy_to_user((int __user*)arg, &size, sizeof(int)))
{
return -EFAULT;
}
1.2 并发控制与mutex的细节
1.2.1 并发导致的后果
在 Linux 系统中,设备文件 /dev/my_cdev_device 可以被多个进程同时 open 并操作。假设 没有 互斥锁:
- 进程 A 调用
read,读取了一半数据,此时 CPU 调度切换。 - 进程 B 恰好调用了
ioctl发送了MYDEV_IOC_RESET指令,执行了memset(dev->buffer, 0, BUFFER_SIZE),并将data_size置0。 - 进程 A 恢复执行,继续从原来的
offset往后读,此时不仅读到了全是0的脏数据,甚至可能因为data_size突变而引发内存越界访问。
1.2.2 mutex的生命周期
值得一提的是,在老版本的 Linux 内核中,这个回调函数就叫做 ioctl,并且它受到 大内核锁 BKL 的保护,如下图,Linux 内核版本 v2.6.0 的 file_operations:

但现代内核为了提高并发性能去掉了 BKL,回调函数改名为 unlocked_ioctl。这意味着,我们必须自己负责并发安全的加锁逻辑。 再看我现在使用的 v5.10.198 版本:

为了解决并发问题,我们在 struct my_cdev 中引入了 struct mutex lock。
需要进行以下操作:
- 在
my_cdev_init中必须使用mutex_init(&my_cdev.lock)初始化。 - 在
my_cdev_exit和错误处理的goto分支中,使用mutex_destroy(&my_cdev.lock)释放资源。
1.2.3 高频踩坑点
互斥锁最容易出错的地方不在于加锁,而在于 解锁的时机 。一旦在某个分支忘记解锁,后续所有的读写进程都会被永远挂起,造成 死锁。
请看 my_cdev_read 中的部分内容:
c
mutex_lock(&dev->lock); //进入临界区,加锁
//读到文件末尾,return之前,必须unlock
if(*offset >= dev->data_size) {
mutex_unlock(&dev->lock);
return 0;
}
len = min(count, (size_t)((dev->data_size)-(*offset)));
//拷贝给用户态发生缺页或内存错误,return之前必须unlock
if(copy_to_user(buf, (dev->buffer) + (*offset), len)) {
mutex_unlock(&dev->lock);
return -EFAULT;
}
*offset += len;
dev->read_count++;
//正常退出之前unlock
mutex_unlock(&dev->lock);
总结成一句话:在编写内核驱动时,凡是处于 mutex_lock 之后的 return 语句,都必须仔细查看是否有对应的 mutex_unlock。
2. unlocked_ioctl回调函数实现
我们还需要在 unlocked_ioctl 回调函数中实现前面两个宏的具体操作,并绑定到 file_operations 结构体上:
c
static long my_cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct my_cdev *dev = file->private_data;
int size;
//检查命令是否属于我们的设备
if(_IOC_TYPE(cmd) != MYDEV_MAGIC)
{
return -ENOTTY;
}
//检查命令号是否有效
if(_IOC_NR(cmd) > 1)
{
return -ENOTTY;
}
switch(cmd)
{
case MYDEV_IOC_RESET://清空缓冲区
{
mutex_lock(&dev->lock);
memset(dev->buffer, 0, BUFFER_SIZE);
dev->data_size = 0;
mutex_unlock(&dev->lock);
printk(KERN_INFO "buffer reset");
break;
}
case MYDEV_IOC_GET_SIZE://获取数据大小
{
mutex_lock(&dev->lock);
size = dev->data_size;
mutex_unlock(&dev->lock);
//把数据大小拷贝给用户程序
if(copy_to_user((int __user*)arg, &size, sizeof(int)))
{
return -EFAULT;
}
break;
}
default:
return -ENOTTY;
}
return 0;
}
static struct file_operations my_cdev_fops = {
.owner = THIS_MODULE,
.open = my_cdev_open,
.release = my_cdev_release,
.read = my_cdev_read,
.write = my_cdev_write,
.llseek = default_llseek,
.unlocked_ioctl = my_cdev_ioctl, //别忘了加进来
};
检查操作我们上面已经讲过了,两个宏的具体实现我们这里使用了 switch-case 完成,核心就是通过判断 cmd 的类型而执行不同的操作,case 中的处理逻辑其实很简单,最重要的是心中一定要有这个框架,这样才能独立完成代码的编写。
3. 应用程序逻辑分析
为了验证驱动逻辑,我编写了一个严格对应的用户态 C 程序。执行流程如下:
- 调用
write写入22字节字符串"This project is ioctl!"。 - 调用
ioctl配合MYDEV_IOC_GET_SIZE命令,成功获取到size = 22。 - 通过
lseek将偏移量归零,读出刚刚写入的字符串。 - 调用
ioctl发送MYDEV_IOC_RESET,内核层在mutex锁的保护下将buffer清零。 - 再次获取大小,发现
size已经归零,再次read,返回值为 0。
应用程序的代码基本都是一些简单的文件操作逻辑,没什么可讲的,完整代码和驱动程序代码一起放到文末了。
4. 运行程序
交叉编译好驱动程序,应用程序可以直接在板子上编译,如下:

我们得到了等会要加载的 ioctl.ko 模块和存放着应用程序的文件夹 test_app。
我们可以另开一个终端使用下面命令实时查看内核日志状况:
c
sudo dmesg -w
然后用下面命令加载模块:
c
sudo insmod ioctl.ko
内核日志如下:

然后我们就可以运行应用程序了,看看和我们预测的结果是否相同:

相应的内核日志如下:

可以看到,和我们预测的结果完全相同。
至此,我们的正文就告一段落。
博主有话说: 感谢阅读!希望这篇文章能帮你在学习 Linux 驱动开发时扫清关于 ioctl 和并发控制的障碍。
如果这篇文章对你有所帮助,请不要吝啬你的👍点赞 和⭐收藏。
关注我,后续持续硬核输出更多 Linux 内核与驱动底层原理解析!有问题欢迎在评论区随时探讨交流~
5. 附录
5.1 驱动程序代码
c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h> //kmalloc
#include <linux/uaccess.h> //copy_from_user copy_to_user
#include <linux/string.h> //memcpy
#include <linux/mutex.h> //互斥锁
#define BUFFER_SIZE 1024
//定义ioctl命令,要和用户程序保持一致
#define MYDEV_MAGIC 'k'
#define MYDEV_IOC_RESET _IO(MYDEV_MAGIC,0) //清空缓冲区
#define MYDEV_IOC_GET_SIZE _IOR(MYDEV_MAGIC, 1, int) //获取数据大小
struct my_cdev{
dev_t dev_num;
struct cdev cdev;
struct class *class;
struct device *device;
char *buffer;
size_t data_size;
struct mutex lock; //互斥锁
//统计信息
unsigned long read_count;
unsigned long write_count;
};
struct my_cdev my_cdev;
static int my_cdev_open(struct inode* inode,struct file* file)
{
//把结构体指针存入file->private_data,方便别的函数使用
file->private_data = &my_cdev;
printk(KERN_INFO "open success!\n");
return 0;
}
static int my_cdev_release(struct inode* inode, struct file* file)
{
printk(KERN_INFO "release success!\n");
return 0;
}
static ssize_t my_cdev_read(struct file* file, char __user* buf, size_t count, loff_t* offset)
{
struct my_cdev* dev = file->private_data;
size_t len;//实际能读的字节数
printk(KERN_INFO "my_cdev: [read] offset=%lld count=%zu data_size=%zu\n", *offset, count, dev->data_size);
mutex_lock(&dev->lock); //加锁
//如果读到文件末尾,返回0,表示文件结束
if(*offset >= dev->data_size)
{
mutex_unlock(&dev->lock); //退出之前要释放互斥锁
return 0;
}
len = min(count,(size_t)((dev->data_size)-(*offset)));
if(copy_to_user(buf, (dev->buffer) + (*offset), len))
{
mutex_unlock(&dev->lock);
return -EFAULT;
}
*offset += len;//更新偏移量
dev->read_count++; //读计数加一
mutex_unlock(&dev->lock);
return len;
}
static ssize_t my_cdev_write(struct file* file, const char __user* buf, size_t count, loff_t* offset)
{
struct my_cdev* dev = file->private_data;
size_t len;
printk(KERN_INFO "my_cdev: [write] offset=%lld count=%zu\n", *offset, count);
mutex_lock(&dev->lock);
//偏移量超过缓冲区大小
if(*offset >= BUFFER_SIZE)
{
mutex_unlock(&dev->lock);
return 0;
}
len = min(count, (size_t)(BUFFER_SIZE - *offset));
if(copy_from_user((dev->buffer)+(*offset), buf, len))
{
mutex_unlock(&dev->lock);
return -EFAULT;
}
*offset += len;
if(*offset >= dev->data_size)
{
dev->data_size = *offset;//更新数据大小
}
dev->write_count++;//写计数加一
mutex_unlock(&dev->lock);
return len;
}
//ioctl函数
static long my_cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct my_cdev *dev = file->private_data;
int size;
//检查命令是否属于我们的设备
if(_IOC_TYPE(cmd) != MYDEV_MAGIC)
{
return -ENOTTY;
}
//检查命令号是否有效
if(_IOC_NR(cmd) > 1)
{
return -ENOTTY;
}
switch(cmd)
{
case MYDEV_IOC_RESET://清空缓冲区
{
mutex_lock(&dev->lock);
memset(dev->buffer, 0, BUFFER_SIZE);
dev->data_size = 0;
mutex_unlock(&dev->lock);
printk(KERN_INFO "buffer reset");
break;
}
case MYDEV_IOC_GET_SIZE://获取数据大小
{
mutex_lock(&dev->lock);
size = dev->data_size;
mutex_unlock(&dev->lock);
//把数据大小拷贝给用户程序
if(copy_to_user((int __user*)arg, &size, sizeof(int)))
{
return -EFAULT;
}
break;
}
default:
return -ENOTTY;
}
return 0;
}
static struct file_operations my_cdev_fops = {
.owner = THIS_MODULE,
.open = my_cdev_open,
.release = my_cdev_release,
.read = my_cdev_read,
.write = my_cdev_write,
.llseek = default_llseek,
.unlocked_ioctl = my_cdev_ioctl, //别忘了加进来
};
static int __init my_cdev_init(void)
{
int ret;
//分配缓冲区
my_cdev.buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if(!my_cdev.buffer)
{
printk(KERN_INFO "kmalloc failed!\n");
return -ENOMEM;
}
memset(my_cdev.buffer, 0, BUFFER_SIZE);
my_cdev.data_size = 0;
my_cdev.read_count = 0;
my_cdev.write_count = 0;
ret = alloc_chrdev_region(&(my_cdev.dev_num), 0, 1, "my_cdev");
if(ret < 0)
{
printk(KERN_INFO "alloc failed!\n");
goto err_alloc;
}
//打印主次设备号
printk(KERN_INFO "Major:%d \t Minor:%d\n",MAJOR(my_cdev.dev_num),MINOR(my_cdev.dev_num));
//初始化互斥锁
mutex_init(&my_cdev.lock);
//cdev初始化
cdev_init(&(my_cdev.cdev), &my_cdev_fops);
my_cdev.cdev.owner = THIS_MODULE;
//cdev添加到内核
ret = cdev_add(&(my_cdev.cdev), my_cdev.dev_num, 1);
if(ret < 0)
{
printk(KERN_INFO "cdev add failed!\n");
goto err_cdev_add;
}
my_cdev.class = class_create(THIS_MODULE, "my_cdev_class");
if(IS_ERR(my_cdev.class))
{
ret = PTR_ERR(my_cdev.class);
printk(KERN_INFO "class create failed!\n");
goto err_class;
}
my_cdev.device = device_create(my_cdev.class, NULL, my_cdev.dev_num, NULL, "my_cdev_device");
if(IS_ERR(my_cdev.device))
{
ret = PTR_ERR(my_cdev.device);
printk(KERN_INFO "device create failed!\n");
goto err_device;
}
printk(KERN_INFO "init success!\n");
return 0;
err_device:
class_destroy(my_cdev.class);
err_class:
cdev_del(&my_cdev.cdev);
err_cdev_add:
mutex_destroy(&my_cdev.lock);//销毁锁
unregister_chrdev_region(my_cdev.dev_num,1);
err_alloc:
kfree(my_cdev.buffer);
return ret;
}
static void __exit my_cdev_exit(void)
{
device_destroy(my_cdev.class, my_cdev.dev_num);
class_destroy(my_cdev.class);
cdev_del(&my_cdev.cdev);
mutex_destroy(&my_cdev.lock);//销毁锁
unregister_chrdev_region(my_cdev.dev_num,1);
kfree(my_cdev.buffer);
printk(KERN_INFO "resource freed!\n");
}
module_init(my_cdev_init);
module_exit(my_cdev_exit);
MODULE_LICENSE("GPL");
5.2 应用程序代码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#define DEV_NAME "/dev/my_cdev_device"
//定义ioctl命令,与驱动程序中一致
#define MYDEV_MAGIC 'k'
#define MYDEV_IOC_RESET _IO(MYDEV_MAGIC, 0)
#define MYDEV_IOC_GET_SIZE _IOR(MYDEV_MAGIC, 1, int)
int main()
{
int fd;
int size;
char buf[128];
fd = open(DEV_NAME,O_RDWR);//可读可写
if(fd < 0)
{
printf("open failed!\n");
return -1;
}
//写入内容
strcpy(buf,"This project is ioctl!");
int ret = write(fd,buf,strlen(buf));
if(ret < 0)
{
printf("write failed!\n");
close(fd);
return -1;
}
printf("写入: %s\n",buf);
//获取数据大小
ioctl(fd, MYDEV_IOC_GET_SIZE, &size);
printf("当前数据大小:%d字节\n",size);
//读取数据
lseek(fd, 0, SEEK_SET);
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf));
printf("当前内容:%s\n",buf);
//清空缓冲区
ioctl(fd,MYDEV_IOC_RESET);
printf("buffet reset\n");
//再次获取数据大小
ioctl(fd, MYDEV_IOC_GET_SIZE, &size);
printf("reset 后数据大小:%d\n",size);
//尝试读取,这次应该读不到内容
lseek(fd, 0, SEEK_SET);
memset(buf, 0, sizeof(buf));
int n = read(fd, buf, sizeof(buf));
printf("reset之后:读取字节数%d \t 读取的内容:%s\n", n, buf);
close(fd);
return 0;
}