linux 字符驱动框架(用户态的read,write,poll是怎么操作驱动的)
前言
这篇文章是通过对一个简单字符设备驱动的操作来解释,用户态的读写操作是怎么映射到具体设备的。
因为针对不同版本的linux内核,驱动的接口函数一直有变化,这贴出我测试的系统信息:
root@ubuntu:~/share/dev/cdev-2# cat /etc/os-release |grep -i ver
VERSION="16.04.5 LTS (Xenial Xerus)"
VERSION_ID="16.04"
VERSION_CODENAME=xenial
root@ubuntu:~/share/dev/cdev-2#
root@ubuntu:~/share/dev/cdev-2# uname -r
4.15.0-33-generic
字符驱动
这里给出了一个不怎么标准的驱动,定义了一个结构体 struct dev,其中buffer成员模拟驱动的寄存器。由wr,rd作为读写指针,len作为缓存buffer的长度。具体步骤如下:
1. 定义 init 函数,exit函数,这是在 insmod,rmmod时候调用的。
2. 定义驱动打开函数open,这是在用户态打开设备时候调用的。
3. 定义release函数,这是在用户态关闭设备时候用到的。
4. 定义read,write,poll函数,并挂接到 file_operations结构体中,所有用户态的read,write,poll都会最终调到这些函数。
chardev.c
/*
参考:深入浅出linux设备驱动开发
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/semaphore.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <linux/poll.h>
#define MAXNUM 100
#define MAJOR_NUM 400 //主设备号 ,没有被使用
struct dev{
struct cdev devm; //字符设备
struct semaphore sem;
int flag;
poll_table* table;
wait_queue_head_t outq;//等待队列,实现阻塞操作
char buffer[MAXNUM+1]; //字符缓冲区
char *rd,*wr,*end; //读,写,尾指针
}globalvar;
static struct class *my_class;
int major=MAJOR_NUM;
static ssize_t globalvar_read(struct file *,char *,size_t ,loff_t *);
static ssize_t globalvar_write(struct file *,const char *,size_t ,loff_t *);
static int globalvar_open(struct inode *inode,struct file *filp);
static int globalvar_release(struct inode *inode,struct file *filp);
static unsigned int globalvar_poll(struct file* filp, poll_table* wait);
/*
结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。
该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。
设备"gobalvar"的基本入口点结构变量gobalvar_fops
*/
struct file_operations globalvar_fops =
{
/*
标记化的初始化格式这种格式允许用名字对这类结构的字段进行初始化,这就避免了因数据结构发生变化而带来的麻烦。
这种标记化的初始化处理并不是标准 C 的规范,而是对 GUN 编译器的一种(有用的)特殊扩展
*/
//用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
.read=globalvar_read,
//发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
.write=globalvar_write,
//尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
.open=globalvar_open,
//当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release () 函数:release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。
.release=globalvar_release,
.poll = globalvar_poll,
};
//内核模块的初始化
static int globalvar_init(void)
{
int result = 0;
int err = 0;
printk("%d\n\n", __LINE__);
dev_t dev = MKDEV(major, 0);
if(major)
{
//静态申请设备编号
result = register_chrdev_region(dev, 1, "charmem1");
}
else
{
//动态分配设备号
result = alloc_chrdev_region(&dev, 0, 1, "charmem1");
major = MAJOR(dev);
}
printk("%d\n\n", __LINE__);
if(result < 0)
{
printk("request or allo devm failed.\n");
return result;
}
//注册字符设备驱动,设备号和file_operations结构体进行绑定
cdev_init(&globalvar.devm, &globalvar_fops);
printk("%d\n\n", __LINE__);
globalvar.devm.owner = THIS_MODULE;
err = cdev_add(&globalvar.devm, dev, 1);
if(err)
printk(KERN_INFO "Error %d adding char_mem device", err);
else
{
printk("globalvar register success\n");
sema_init(&globalvar.sem,1); //初始化信号量
init_waitqueue_head(&globalvar.outq); //初始化等待队列
globalvar.rd = globalvar.buffer; //读指针
globalvar.wr = globalvar.buffer; //写指针
globalvar.end = globalvar.buffer + MAXNUM;//缓冲区尾指针
globalvar.flag = 0; // 阻塞唤醒标志置 0
printk("%d\n\n", __LINE__);
}
/*
定义在/include/linux/device.h
创建class并将class注册到内核中,返回值为class结构指针
在驱动初始化的代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。
省去了利用mknod命令手动创建设备节点
*/
my_class = class_create(THIS_MODULE, "chardev1");
device_create(my_class, NULL, dev, NULL, "chardev1");
printk("%d\n\n", __LINE__);
return 0;
}
static int globalvar_open(struct inode *inode,struct file *filp)
{
try_module_get(THIS_MODULE);//模块计数加一
printk("This chrdev is in open\n");
return(0);
}
static int globalvar_release(struct inode *inode,struct file *filp)
{
module_put(THIS_MODULE); //模块计数减一
printk("This chrdev is in release\n");
return(0);
}
static void globalvar_exit(void)
{
device_destroy(my_class, MKDEV(major, 0));
class_destroy(my_class);
cdev_del(&globalvar.devm);
/*
参数列表包括要释放的主设备号和相应的设备名。
参数中的这个设备名会被内核用来和主设备号参数所对应的已注册设备名进行比较,如果不同,则返回 -EINVAL。
如果主设备号超出了所允许的范围,则内核同样返回 -EINVAL。
*/
unregister_chrdev_region(MKDEV(major, 0), 1);//注销设备
}
static ssize_t globalvar_read(struct file *filp,char *buf,size_t len,loff_t *off)
{
if(wait_event_interruptible(globalvar.outq, globalvar.flag!=0)) //不可读时 阻塞读进程
{
return -ERESTARTSYS;
}
if(down_interruptible(&globalvar.sem)) //P 操作
{
return -ERESTARTSYS;
}
globalvar.flag = 0;
printk("into the read function\n");
printk("the rd is %c\n",*globalvar.rd); //读指针
if(globalvar.rd < globalvar.wr)
len = min(len,(size_t)(globalvar.wr - globalvar.rd)); //更新读写长度
else
len = min(len,(size_t)(globalvar.end - globalvar.rd));
printk("the len is %lu\n",len);
if(copy_to_user(buf,globalvar.rd,len))
{
printk(KERN_ALERT"copy failed\n");
/*
up递增信号量的值,并唤醒所有正在等待信号量转为可用状态的进程。
必须小心使用信号量。被信号量保护的数据必须是定义清晰的,并且存取这些数据的所有代码都必须首先获得信号量。
*/
up(&globalvar.sem);
return -EFAULT;
}
printk("the read buffer is %s\n",globalvar.buffer);
globalvar.rd = globalvar.rd + len;
if(globalvar.rd == globalvar.end)
globalvar.rd = globalvar.buffer; //字符缓冲区循环
up(&globalvar.sem); //V 操作
return len;
}
static ssize_t globalvar_write(struct file *filp,const char *buf,size_t len,loff_t *off)
{
if(down_interruptible(&globalvar.sem)) //P 操作
{
return -ERESTARTSYS;
}
if(globalvar.rd <= globalvar.wr)
len = min(len,(size_t)(globalvar.end - globalvar.wr));
else
len = min(len,(size_t)(globalvar.rd-globalvar.wr-1));
printk("the write len is %lu\n",len);
if(copy_from_user(globalvar.wr,buf,len))
{
up(&globalvar.sem); //V 操作
return -EFAULT;
}
printk("the write buffer is %s\n",globalvar.buffer);
printk("the len of buffer is %lu\n",strlen(globalvar.buffer));
globalvar.wr = globalvar.wr + len;
if(globalvar.wr == globalvar.end)
globalvar.wr = globalvar.buffer; //循环
up(&globalvar.sem);
//V 操作
globalvar.flag=1; //条件成立,可以唤醒读进程
wake_up_interruptible(&globalvar.outq); //唤醒读进程
return len;
}
static unsigned int globalvar_poll(struct file* filp, poll_table* wait)
{
unsigned int mask = 0;
poll_wait(filp, &globalvar.outq, wait);
if (globalvar.wr != globalvar.rd)
{
mask |= POLLIN|POLLRDNORM;
printk("globalvar_poll raise readable\n");
return mask;
}
else
{
mask |= POLLOUT|POLLWRNORM;
printk("globalvar_poll raise writeable\n");
return mask;
}
return 0;
}
module_init(globalvar_init);
module_exit(globalvar_exit);
MODULE_LICENSE("GPL");
对应的Makefile:
obj-m := cdev.o
PWD := $(shell pwd)
KDIR := /lib/modules/$(shell uname -r)/build
default: clean
$(MAKE) -C $(KDIR) M=$(PWD) CC=gcc-5 modules
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions Module.* modules.order .cache.mk
编译:
root@ubuntu:~/share/dev/cdev-2# make
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions Module.* modules.order .cache.mk
make -C /lib/modules/4.15.0-33-generic/build M=/mnt/hgfs/share/dev/cdev-2 CC=gcc-5 modules
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-33-generic'
CC [M] /mnt/hgfs/share/dev/cdev-2/cdev.o
/mnt/hgfs/share/dev/cdev-2/cdev.c: In function ‘globalvar_init’:
/mnt/hgfs/share/dev/cdev-2/cdev.c:64:5: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
dev_t dev = MKDEV(major, 0);
^
Building modules, stage 2.
MODPOST 1 modules
CC /mnt/hgfs/share/dev/cdev-2/cdev.mod.o
LD [M] /mnt/hgfs/share/dev/cdev-2/cdev.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-33-generic'
root@ubuntu:~/share/dev/cdev-2# insmod cdev.ko
具体的 log 这里没有给出来但是会在 /var/log/kern.log 记录:
root@ubuntu:~/share/dev/cdev-2# tail -f /var/log/kern.log
Sep 12 16:45:25 ubuntu kernel: [48365.325205]
Sep 12 16:45:25 ubuntu kernel: [48365.325213] globalvar register success
Sep 12 16:45:25 ubuntu kernel: [48365.329010]
用户态函数
用户态 select 机制
reader-writer.c
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/time.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int fd;
fd_set rfd, wfd;
struct timeval timeout = {3,0};
char msg[101];
fd= open("/dev/chardev1",O_RDWR,S_IRUSR|S_IWUSR);
if(fd!=-1)
{
while(1)
{
bzero(msg, 100);
FD_ZERO(&rfd);
FD_SET(fd, &rfd);
FD_ZERO(&wfd);
FD_SET(fd, &wfd);
switch(select(fd+1, &rfd, &wfd, NULL, &timeout))
{
case -1:
printf("error ...\n");
exit(1);
case 0:
printf("timeout\n");
break;
default:
if(FD_ISSET(fd, &wfd))
{
printf("write...\n");
scanf("%s",msg);
write(fd,msg,strlen(msg));
}
if(FD_ISSET(fd, &rfd))
{
printf("read..\n");
read(fd,msg,100);
printf("%s\n",msg);
}
break;
}
}
}
else
{
printf("device open failure,%d\n",fd);
}
close(fd);
return 0;
}
用户态简单读写机制
一下是简单地read,write操作。
writer.c
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fd;
char msg[100];
fd= open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR);
if(fd!=-1)
{
while(1)
{
printf("Please input the globar:\n");
scanf("%s",msg);
write(fd,msg,strlen(msg));
if(strcmp(msg,"quit")==0)
{
close(fd);
break;
}
}
}
else
{
printf("device open failure\n");
}
return 0;
}
reader.c
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fd,i;
char msg[101];
fd= open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR);
if(fd!=-1)
{
while(1)
{
for(i=0;i<101;i++)
msg[i]='\0';
read(fd,msg,100);
printf("%s\n",msg);
if(strcmp(msg,"quit")==0)
{
close(fd);
break;
}
}
}
else
{
printf("device open failure,%d\n",fd);
}
return 0;
}
测试结论
这里不贴测试图,可自行复制代码,编译测试。
前面利用 poll 机制不会有超时事件,这是因为我们测试是用驱动层面数组模拟的。
当读写指针不等表示可写,否则为可读,所以不存在其他情况,poll每次总能获得可读可写的返回。