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每次总能获得可读可写的返回。
linux 字符驱动框架(用户态的read,write,poll是怎么操作驱动的)的更多相关文章
- linuxok6410的I2C驱动分析---用户态驱动
3 i2c-dev 3.1 概述 之前在介绍I2C子系统时,提到过使用i2c-dev.c文件在应用程序中实现我们的I2C从设备驱动.不过,它实现的是一个虚拟,临时的i2c_client,随着设备文件 ...
- Linux字符界面下用户账户的设置
在Linux系统字符界面下创建.修改以及删除用户账户主要使用useradd,usermod和userdel这3个命令. 一.创建用户账户 创建用户账户就是在系统中创建一个新账户,然后为新账户分配用户U ...
- Linux操作系统学习_用户态与内核态之切换过程
因为操作系统的很多操作会消耗系统的物理资源,例如创建一个新进程时,要做很多底层的细致工作,如分配物理内存,从父进程拷贝相关信息,拷贝设置页目录.页表等,这些操作显然不能随便让任何程序都可以做,于是就产 ...
- 聊聊Linux用户态驱动设计
序言 设备驱动可以运行在内核态,也可以运行在用户态,用户态驱动的利弊网上有很多的讨论,而且有些还上升到政治性上,这里不再多做讨论.不管用户态驱动还是内核态驱动,他们都有各自的缺点.内核态驱动的问题是: ...
- Linux用户态驱动设计
聊聊Linux用户态驱动设计 序言 设备驱动可以运行在内核态,也可以运行在用户态,用户态驱动的利弊网上有很多的讨论,而且有些还上升到政治性上,这里不再多做讨论.不管用户态驱动还是内核态驱动,他们都 ...
- Linux I2C驱动--用户态驱动简单示例
1. Linux内核支持I2C通用设备驱动(用户态驱动:由应用层实现对硬件的控制可以称之为用户态驱动),实现文件位于drivers/i2c/i2c-dev.c,设备文件为/dev/i2c-0 2. I ...
- Linux Framebuffer驱动剖析之二—驱动框架、接口实现和使用
深入分析LinuxFramebuffer子系统的驱动框架.接口实现和使用. 一.LinuxFramebuffer的软件需求 上一篇文章详细阐述了LinuxFramebuffer的软件需求(请先理解第一 ...
- Linux块设备驱动(二) _MTD驱动及其用户空间编程
MTD(Memory Technology Device)即常说的Flash等使用存储芯片的存储设备,MTD子系统对应的是块设备驱动框架中的设备驱动层,可以说,MTD就是针对Flash设备设计的标准化 ...
- [中英对照]Device Drivers in User Space: A Case for Network Device Driver | 用户态设备驱动: 以网卡驱动为例
前文初步介绍了Linux用户态设备驱动,本文将介绍一个典型的案例.Again, 如对Linux用户态设备驱动程序开发感兴趣,请阅读本文,否则请飘过. Device Drivers in User Sp ...
随机推荐
- JavaScript基础-04-对象、函数
对象 1. 对象:使用基本数据类型的数据,创建的变量都是独立的,不能成为一个整体 对象属于一个复合数据类型,在对象中可以保存多个不同数据类型的属性. 对象的分类: (1)内建对象:由ES ...
- sourcetree关于注册的问题
当前只有Win的版本,Mac自行百度(笑) 很多人用git命令行不熟练,那么可以尝试使用sourcetree进行操作. 然鹅~~sourcetree又一个比较严肃的问题就是,很多人不会跳过注册或者操作 ...
- Elasticsearch聚合语句
聚合的范围是search query过滤出的数据 四种聚合类型: 一.Bucketing 桶聚合,常规的分类然后计算每个分类的文档数量 二.Metric 分类并对一组文档进行sum.avg等数学运算 ...
- linux驱动之内核多线程(四)
本文摘自 http://www.cnblogs.com/zhuyp1015/archive/2012/06/13/2548494.html 自己创建的内核线程,当把模块加载到内核之后,可以通过:ps ...
- CAS和锁的相关面试题
CAS 锁 锁的四种状态和升级 锁的四种状态:无锁.偏向锁.轻量级锁和重量级锁 无锁 无锁就是没有真正意义上的上锁,所有的线程还是能访问并修改同一个资源,但是通过算法控制,实现同时只有一个线程修改成功 ...
- Micro LED巨量转移技术研究进展
近年来,Micro LED因其功耗低.响应快.寿命长.光效率高等特点,被视为继LCD.OLED之后的新一代显示面板技术.Micro LED的英文全名是Micro Light Emitting Diod ...
- 从Vessel到二代裸金属容器,云原生的新一波技术浪潮涌向何处?
摘要:云原生大势,深度解读华为云四大容器解决方案如何加速技术产业融合. 云原生,可能是这两年云服务领域最火的词. 相较于传统的应用架构,云原生构建应用简便快捷,部署应用轻松自如.运行应用按需伸缩,是企 ...
- Java面试题 200+
面试题包含的内容了十九了模块:Java 基础.容器.多线程.反射.对象拷贝.Java Web 模块.异常.网络.设计模式.Spring/Spring MVC.Spring Boot/Spring Cl ...
- openCV - 4. 图像操作
读写图像.读写像素.修改像素值 读写图像 imread 可以指定加载为灰度或者RGB图像 Imwrite 保存图像文件,类型由扩展名决定 读写像素 读一个GRAY像素点的像素值(CV_8UC1) Sc ...
- go语言之函数及闭包
一:函数 1 概述: 函数是 Go 程序源代码的基本构造单位,一个函数的定义包括如下几个部分,函数声明关键字 也町. 函数名.参数列表.返回列表和函数体.函数名遵循标识符的命名规则, 首字母的大小写决 ...