实验内容

编写一个简单的字符设备驱动程序,该字符设备并不驱动特定的硬件, 而是用内核空间模拟字符设备,要求该字符设备包括以下几个基本操作,打开、读、写和释放,并编写测试程序用于测试所编写的字符设备驱动程序。在此基础上,编写程序实现对该字符设备的同步操作。

相关知识

设备驱动程序

设备驱动程序是内核和硬件设备之间的接口,设备驱动程序屏蔽硬件细节,且设备被映射成特殊的文件进行处理。每个设备都对应一个文件名,在内核中也对应一个索引节点,应用程序可以通过设备的文件名来访问硬件设备。Linux 为文件和设备提供了一致性的接口,用户操作设备文件和操作普通文件类似。例如通过 open() 函数可打开设备文件,建立起应用程序与目标程序的连接;之后,可以通过read() 、write()等常规文件函数对目标设备进行数据传输操作。设备驱动程序封装了如何控制设备的细节,它们可以使用和操作文件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O 控制等操作,而驱动程序的主要任务也就是要实现这些系统调用函数。设备驱动程序是一些函数和数据结构的集合,这些函数和数据结构是为实现设备管理的一个简单接口。操作系统内核使用这个接口来请求驱动程序对设备进行I/O操作。

字符驱动程序相关数据结构

字符设备是以字节为单位逐个进行I/O 操作的设备,不经过系统的 I/O 缓冲区,所以需要管理自己的缓冲区结构。

在字符设备驱动程序中,主要涉及 3 个重要的内核数据结构,分别是file_operations、file和inode。当用户访问设备文件时,每个文件的file结构都有一个索引节点inode与之对应。在内核的inode结构中,有一个名为i_fop成员,其类型为file_operations。file_operations定义文件的各种操作,用户对文件进行诸如open、close、read、write等操作时,Linux 内核将通过file_operations结构访问驱动程序提供的函数。内核通过这3个数据结构的关联,将用户对设备文件的操作转换为对驱动程序相关函数的调用。

file 代表一个打开了的文件。它由内核在使用 open()函数时建立,并传递给该文件上进行操作的所有函数,直到最后的close()函数。当文件的所有操作结束后,内核会释放该数据结构。

内核用inode结构在内部标识文件,它和file结构不同,后者标识打开的文件描述符。对于单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向同一个inode结构,inode结构中有两个重要的字段:

struct cdev *i_cdev;
dev_t i_rdev;

类型dev_t描述设备号,cdev结构表示字符设备的内核数据结构,内核不推荐开发者直接通过结构的i_rdev结构获得主次设备号,而提供下面两个函数取得主、次设备号。

Unsigned int iminor(struct inode *inode);
Unsigned int imajor(struct inode *inode);

设备文件的创建

实验中需要对一个设备文件进行操作,这里有两种方案,一是在终端中人工使用命令进行创建,二是直接在模块程序中予以创建。

并发控制

在驱动程序中,当多个线程同时访问相同的资源时(驱动程序中的全局变量是一种典型的共享资源),可能会引发“ 竞争” ,因此必须对共享资源进行并发控制。在此驱动程序中,可用信号量机制来实现并发控制。

相关结构与函数

设备驱动程序结构

字符设备的结构描述如下:

struct Scull_Dev{ //驱动程序结构体struct cdev devm; //字符设备 struct semaphore sem; //信号量,实现读写时的 PV 操作 wait_queue_head_t outq; //等待队列,实现阻塞操作 int flag; //阻塞唤醒条件 char buffer[MAXNUM+1]; //字符缓冲区 char *rd,*wr,*end; //读,写,尾指针
};

scull(simple character utility for loading localities,“区域装载的简单字符工具”)是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个字符设备。scull的优点在于他不和任何硬件相关,而只是操作从内核分配的一些内存。任何人都可以编译和运行scull,而且还看看可以将scull移植到linux支持的所有计算机平台上。但另一方面,除了展示内核和字符设备驱动程序之间的接口并且让用户运行某些测试例程外,scull设备做不了任何“有用”的事情。

字符设备的数据接口

字符设备的数据接口,将文件的读、写、打开、释放等操作映射为相应的函数。

struct file_operations globalvar_fops =
{
.read=globalvar_read,
.write=globalvar_write,
.open=globalvar_open,
.release=globalvar_release,
};

字符设备的注册与注销

字符设备的注册采用静态申请和动态分配相结合的方式,使用register_chrdev_region函数和alloc_chrdev_region函数来完成。字符设备的注销采用unregister_chrdev_region函数来完成。

字符设备的打开与释放

打开设备是通过调用file_operations结构中的函数open()来完成的。设备的打开提供给驱动程序初始化的能力,从而为以后的操作准备,此外还会递增设备的使用计数,防止在文件被关闭前被卸载出内核。

释放设备是通过调用 file_operations 结构中的函数 release()来完成的。设备的释放作用刚好与打开相反,但基本的释放操作只包括设备的使用计数递减。

字符设备的读写操作

直接使用函数read( )和write( )。文件读操作的原型如下:

Ssize_t device_read(struct file* filp,char __user* buff,size_t le n,loff_t* offset);

其中,filp 是文件对象指针;buff 是用户态缓冲区,用来接受读到的数据;len 是希望读取的数据量;offset 是用户访问文件的当前偏移。文件写操作的原型和读操作没有区别,只是操作方向改变而已。由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到用户空间的复制。

实验步骤

编写模块程序

首先编写模块程序,命名为globalvar.c。

定义虚拟字符设备驱动

struct Scull_Dev globalvar; 定义了一个虚拟字符设备驱动globalvar。

声明虚拟字符设备的读、写、打开和释放操作函数

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);

将文件的读、写、打开、释放等操作映射为虚拟字符设备的函数

struct file_operations globalvar_fops =
{ .read=globalvar_read, .write=globalvar_write, .open=globalvar_open, .release=globalvar_release,
};

对设备进行初始化

函数static int globalvar_init(void)对设备进行初始化。一是获取设备号,使用dev_t dev = MKDEV(major, 0);定义一个设备号,用

if(major) { //静态申请设备编号//第一个参数表示设备号,第二个参数表示注册的此设备数目,//第三个表示设备名称。result = register_chrdev_region(dev, 1, "charmem"); } else { //动态分配设备号//第一个参数保存生成的设备号,第二个参数表示次设备号的基准,//即从哪个次设备号开始分配,第三个表示注册的此设备数目,//第四个表示设备名称。result = alloc_chrdev_region(&dev, 0, 1, "charmem"); major = MAJOR(dev);} //返回值:小于0,则自动分配设备号错误。否则分配得到的设备号就被&dev带出来。if(result < 0) return result;

申请或分配设备号。用class_create和device_create创建设备文件。

    my_class = class_create(THIS_MODULE, "chardev0"); device_create(my_class, NULL, dev, NULL, "chardev0");

向内核里面注册驱动

第一个输入参数代表即将被添加入Linux内核系统的字符设备,第二个输入参数是dev_t类型的变量,此变量代表设备的设备号,第三个输入参数是无符号的整型变量,代表想注册设备的设备号的范围。如果成功,则返回0,如果失败,则返回ENOMEM, ENOMEM的被定义为12。

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
}

打开操作

具体为模块计数加一。

static int globalvar_open(struct inode *inode,struct file *filp)
{ //如果该模块处于活动状态且对它引用计数加1操作正确则返回1,否则返回0.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);
}

注销设备操作

具体为用device_destroy注销创建的设备,用class_destroy注销设备类,用cdev_del释放cdev结构体空间,用unregister_chrdev_region来注销设备号。

static void globalvar_exit(void)
{ //注销设备device_destroy(my_class, MKDEV(major, 0)); class_destroy(my_class);cdev_del(&globalvar.devm); unregister_chrdev_region(MKDEV(major, 0), 1); //注销设备
}

读操作

struct file结构

Linux–struct file结构体 - Sophie_h - 博客园 (cnblogs.com)

void *private_data;

​ open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须

记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.

(39条消息) kernel struct file结构中的private_data_huashibuliao的博客-CSDN博客_private_data

private_data 其实是用来保存自定义设备结构体的地址的。自定义结构体的地址被保存在private_data后,可以在read ,write 等驱动函数中被传递和调用自定义设备结构体中的成员。

globalvar.flag标志当前是否可读,若不可读,则用wait_event_interruptible将其挂起到等待队列globalvar.outq。如果可以读,则使用down_interruptible(&globalvar.sem)进行P操作。接下来更新读指针,len是读的字节数,如果读指针小于写指针,表明新写入了内容,应该读取从写指针到读指针的内容,即len = min(len,(size_t)(globalvar.wr - globalvar.rd))。如果如指针大于等于写指针,表明在循环缓冲区中写指针已经过了一次循环,应令len = min(len,(size_t)(globalvar.end - globalvar.rd))将当前读指针到结尾的内容读完,下一次再读到写指针。用copy_to_user(buf,globalvar.rd,len)函数将内核空间的数据读取出来,并更新读指针位置,如果读指针在缓冲区末尾则将其循环地置为缓冲区首部。最后进行V操作退出临界区。

static ssize_t globalvar_read(struct file *filp,char *buf,size_t len,loff_t *off)
{ //globalvar.flag是阻塞唤醒标志,为0可读//条件condition为真时调用这个函数将直接返回0    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 %d\n",len); //copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到//用户空间的复制。如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。if(copy_to_user(buf,globalvar.rd,len)) { printk(KERN_ALERT"copy failed\n"); up(&globalvar.sem); //V操作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;
}

写操作

写操作和读操作大致相同,只不过读写方向相反。首先用P操作进入临界区,计算写入长度len。如果读指针小于写指针,则可以令len = min(len,(size_t)(globalvar.end - globalvar.wr)); 表示从当前写指针到缓冲区末尾都可以写入。如果读指针大于写指针,则只能写入从写指针到读指针之前的位置,即len = min(len,(size_t)(globalvar.rd-globalvar.wr-1));,否则会破坏还未读取的内容,读取时也不能读取写指针后面、本应读取到的内容。最后更新写指针,进行V操作退出临界区,通过 wake_up_interruptible(&globalvar.outq)唤醒读进程。采用这种方式实际上写入的优先级更高。

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 %d\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 %d\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;
}

模块程序

将以上部分汇总得到globalvar.c如下。

#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>
#define MAXNUM 100
#define MAJOR_NUM 456 //主设备号 ,没有被使用
//设备的结构体
struct Scull_Dev{ struct cdev devm; //字符设备 struct semaphore sem; //信号量,实现读写时的 PV 操作 wait_queue_head_t outq; //等待队列,实现阻塞操作 int flag; //阻塞唤醒标志char buffer[MAXNUM+1]; //字符缓冲区 char *rd,*wr,*end; //读,写,尾指针
};
//虚拟字符设备globalvar
struct Scull_Dev globalvar;
static struct class *my_class;
//MAJOR_NUM=456,主设备号
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);
//字符设备的数据接口,将文件的读、写、打开、释放等操作映射为相应的函数。
struct file_operations globalvar_fops =
{ .read=globalvar_read, .write=globalvar_write, .open=globalvar_open, .release=globalvar_release,
};
/*
设备初始化:
调用内核函数register_chrdev把驱动程序的基本入口点
指针存放在内核的字符设备地址表中,在用户进程对该设备
执行系统调用时提供入口地址
*/
static int globalvar_init(void)
{ int result = 0; int err = 0; //MKDEV获取设备在设备表中的位置//major是主设备号,minor是次设备号//这里是新定义一个设备号()dev_t dev = MKDEV(major, 0); if(major) { //静态申请设备编号//第一个参数表示设备号,第二个参数表示注册的此设备数目,//第三个表示设备名称。result = register_chrdev_region(dev, 1, "charmem"); } else { //动态分配设备号//第一个参数保存生成的设备号,第二个参数表示次设备号的基准,//即从哪个次设备号开始分配,第三个表示注册的此设备数目,//第四个表示设备名称。result = alloc_chrdev_region(&dev, 0, 1, "charmem"); major = MAJOR(dev);} //返回值:小于0,则自动分配设备号错误。否则分配得到的设备号就被&dev带出来。if(result < 0) return result; //将struct cdev类型的结构体变量和file_operations结构体进行绑定。cdev_init(&globalvar.devm, &globalvar_fops); //cdev中的struct module *owner;填充时,值要为 THIS_MODULE,表示模块globalvar.devm.owner = THIS_MODULE; //向内核里面添加一个驱动,注册驱动//第一个输入参数代表即将被添加入Linux内核系统的字符设备//第二个输入参数是dev_t类型的变量,此变量代表设备的设备号//第三个输入参数是无符号的整型变量,代表想注册设备的设备号的范围//如果成功,则返回0,如果失败,则返回ENOMEM, ENOMEM的被定义为12。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 } //创建设备文件my_class = class_create(THIS_MODULE, "chardev0"); device_create(my_class, NULL, dev, NULL, "chardev0");return 0;
}
//.open=globalvar_open, 函数映射
static int globalvar_open(struct inode *inode,struct file *filp)
{ //如果该模块处于活动状态且对它引用计数加1操作正确则返回1,否则返回0.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); unregister_chrdev_region(MKDEV(major, 0), 1); //注销设备
} static ssize_t globalvar_read(struct file *filp,char *buf,size_t len,loff_t *off)
{ //globalvar.flag是阻塞唤醒标志,为0可读//条件condition为真时调用这个函数将直接返回0    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 %d\n",len); //copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到//用户空间的复制。如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。if(copy_to_user(buf,globalvar.rd,len)) { printk(KERN_ALERT"copy failed\n"); up(&globalvar.sem); //V操作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 %d\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 %d\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;
} module_init(globalvar_init);
module_exit(globalvar_exit);
MODULE_LICENSE("GPL");

编写读程序

读程序read.c代码如下。

#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
main(void)
{ int fd,i; char num[101]; //打开文件fd = open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR); if(fd!=-1) { while(1) { for(i=0;i<101;i++) num[i]='\0'; read(fd,num,100); printf("%s\n",num); if(strcmp(num,"quit")==0) { close(fd); break; }} } else { printf("device open failure,%d\n",fd); }
}

该读程序首先采用打开文件的方式打开字符设备,若打开失败则返回-1并退出,若打开成功则通过read函数读取设备中的缓冲区,read函数的具体操作被驱动映射为globalvar.read()函数。如果读取到quit则退出程序。

编写写程序

写程序write.c代码如下

#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
main()
{ int fd; char num[100]; fd = open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR); if(fd!=-1) { while(1) { printf("Please input the globalvar:\n"); scanf("%s",num); write(fd,num,strlen(num)); if(strcmp(num,"quit")==0) { close(fd); break; } } } else { printf("device open failure\n"); }
}

写程序与读程序非常相似,只是读写的方向有所改变。

编写Makefile文件

为了编译模块程序globalvar.c,我们建立makefile文件如下

ifneq ($(KERNELRELEASE),)
obj-m := globalvar.o
else
KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

编译模块程序globalvar.c

使用make命令编译模块文件,结果如图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IsVqcalo-1639148931258)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=M2UyOTQ1MTE5MGQ1ZmY5MmNhNmQyOTQ5ZTEwYTczZTVfUEc3TTB4QWpoUzQ5TkF3bG9iSXhqRVNRaXRoVWZObFBfVG9rZW46Ym94Y25TeG5oNFpzQVNnWDFoR2o0VU52bTRjXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

图 编译模块文件

加载模块

使用命令insmod globalvar.ko加载模块程序,值得注意的是加载模块程序需要root权限。加载模块程序如图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Q2kDHeE-1639148931259)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=MWE2MzAyYTk2NGQ0MjJlNTAzNTMzYmI5ZWFiNjA3NGFfb1lQamgxeElwSnNLQnM4VHdaSUNMNHV5b0NWTHdFMEhfVG9rZW46Ym94Y25kdGlubUJZVlJyaDZVbHBSSUtHa1JiXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

图 加载模块程序

编译并打开读写程序

打开两个终端,分别编译并打开读写程序,可以看到打开设备失败,如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I13Nfa64-1639148931260)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=M2Y0YTk4MGU0ODEyODc1YzE1N2Y5NWMyYWYxYzE3YTVfYTVkOU12S29yRjJOTDNZZG9rV3dEWWlXRWlKNFloSUlfVG9rZW46Ym94Y25hTlJ3aElYb0xQbkpFN0dxdW5HdDJjXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

这是权限不足造成的,切换root权限后可以在正常打开。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6SXwTdb9-1639148931260)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=OTVkYTFmNjk0NmJlMGI2NjU4Y2Y1Y2MyMjdmZmM5MjdfNW13Q1NQVlhHRWk5VjN3YlFEUERPeWpyY1dLYTVyODVfVG9rZW46Ym94Y256ZURmSUI0MkhtMXRFcmw4b2I5WW9mXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

打开后在写窗口写入,便可以在读窗口看到输出内容。

但是这样不能实现两个窗口的通信,为此我们需要编写既能读又能写的程序。

编写读写程序

编写读写程序read_write.c,

#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
int fd,i;
char msg[101];
void printbar()
{printf("---------------------------------------------\n");
}
int main()
{printbar();printf("本程序可以对设备进行读写,");fd = open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR);int operation;while(1){printf("请选择您的操作:按1读取信息,按2写入信息,按3退出。\n");printbar();scanf("%d",&operation);getchar();switch (operation){case 1:{printf("您选择了:读取信息。\n");if(fd!=-1){{for(i=0;i<101;i++)  //初始化msg[i]='\0';printf("读取的信息是:");read(fd,msg,100);printf("%s\n",msg);printbar();}}else{printf("设备打开失败!%d\n",fd);printbar();exit(1);}continue;}case 2:{printf("您选择了:写入信息。\n");if(fd!=-1){printf("请输入要写入的信息:\n");scanf("%s",msg);write(fd,msg,strlen(msg));printf("您写入了%s。\n",msg);printbar();}else{printf("设备打开失败!\n");printbar();exit(1);}continue;}case 3:{printf("再见!\n");printbar();return 0;}default:{printf("不支持该操作!\n");printbar();continue;}}}
}

实验结果如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VR9tr1md-1639148931261)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=MDM4OWYwNGRjMGRmYzZmODdhNDdiY2M1M2Y3ZTJhM2NfUFdvQmlSUHpteHhWOVcwSXhDNTB3dzRKakltUkpGdWZfVG9rZW46Ym94Y25XSG1ZT3FreU0zeXNXUkxxaDcxM21kXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aRHkK9TF-1639148931261)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=Y2YzZjcxYTRkNWMzY2YyMGI4MjQzNDc2YjkzNzc5MzJfdml6SnUzUWw2dE9iS1U0cmI0RDJ6NmdYQ3hSc0x6dExfVG9rZW46Ym94Y25lU3hmMm1KbUxBOXBPdlZjcmdMTlBmXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

卸载模块

使用rmmod globalvar卸载模块。

遇到的问题

sudo apt-get下载很慢

参见以下链接

https://blog.csdn.net/bean_business/article/details/112253928

进入超级文件管理器

要进入有root权限的文件管理器,可以在终端中执行

sudo nautilus

什么是ssize_t

(39条消息) ssize_t和size_t详解_lplp90908的博客-CSDN博客_size_t和ssize_t

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,有没有注意到,它和long数据类型有啥区别?其实就是一样的。size_t 就是无符号型的ssize_t,也就是unsigned long/ unsigned int (在32位下),不同的编译器或系统可能会有区别,主要是因为在32位机器上int和long是一样的。在64位没有测试,但是参见百度百科,size_t 在64位下是64位,那么size_t的正确定义应该是typedef unsigned long size_t。

(39条消息) Size_t和int区别_Sambeau-CSDN博客_size_t和int

(1)size_t和int

​ size_t是一些C/C++标准在stddef.h中定义的。这个类型足以用来表示对象的大小。size_t的真实类型与操作系统有关。

在32位架构中被普遍定义为:

typedef unsigned int size_t;

而在64位架构中被定义为:

typedef unsigned long size_t;

​ size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。而int在不同架构下都是4字节,与size_t不同;且int为带符号数,size_t为无符号数。

(2)ssize_t

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int.

(3)size_t和ssize_t作用

​ size_t一般用来表示一种计数,比如有多少东西被拷贝等。例如:sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小。 它的意义大致是“适于计量内存中可容纳的数据项目个数的无符号整数类型”。所以,它在数组下标和内存管理函数之类的地方广泛使用。

​ 而ssize_t这个数据类型用来表示可以被执行读写操作的数据块的大小.它和size_t类似,但必须是signed.意即:它表示的是signed size_t类型的。

什么是static struct class

(39条消息) Linux内核中的 struct class 简介_一程山水一程歌-CSDN博客

(39条消息) linux内核之class介绍(一)_qq_36412526的博客-CSDN博客

class 指的是 设备类(device classes),是对于设备的高级抽象。但 实际上 class 也是一个结构体,只不过 class 结构体在声明时是按照类的思想来组织其成员的。

​ 运用 class,可以让用户空间的程序根据自己要处理的事情来调用设备,而不是根据设备被接入到系统的方式或设备的工作原理来调用。

​ class 结构体的原型和相关描述可以在 linux-4.3/include/linux/device.h 中找到

struct class {const char *name; // 类名称struct module *owner; // 类所属的模块,比如 usb模块、led模块等struct class_attribute          *class_attrs;      // 类所添加的属性const struct attribute_group    **dev_groups;      // 类所包含的设备所添加的属性struct kobject                  *dev_kobj;         // 用于标识 类所包含的设备属于块设备还是字符设备int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);    // 用于在设备发出 uevent 消息时添加环境变量char *(*devnode)(struct device *dev, umode_t *mode);    // 设备节点的相对路径名void (*class_release)(struct class *class);    // 类被释放时调用的函数void (*dev_release)(struct device *dev);       // 设备被释放时调用的函数int (*suspend)(struct device *dev, pm_message_t state);    // 设备休眠时调用的函数int (*resume)(struct device *dev);    // 设备被唤醒时调用的函数const struct kobj_ns_type_operations *ns_type;const void *(*namespace)(struct device *dev);const struct dev_pm_ops *pm;    // 用于电源管理的函数struct subsys_private *p;     // 指向 class_private 结构的指针
};

什么是class_create, class_destroy, device_create, device_destroy

004_linux驱动之_class_create创建一个设备类 - 陆小果哥哥 - 博客园 (cnblogs.com)

linux中class_create和class_register说明 - 如果天空不死 - 博客园 (cnblogs.com)

class_create是创建设备类,而device_create是创建设备,这两个是不一样的。

内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。

device_destroy参考文档如下。

DEVICE_DESTROY(9)

NAME

​ device_destroy - removes a device that was created with device_create

SYNOPSIS

​ void device_destroy(struct class * class, dev_t devt);

ARGUMENTS

​ class

​ pointer to the struct class that this device was registered with

​ devt

​ the dev_t of the device that was previously registered

DESCRIPTION

​ This call unregisters and cleans up a device that was created with a call to device_create.

使用device_destroy和class_destroy来销毁创建的设备和类。

什么是globalvar

假设一个非常简单的虚拟字符设备:这个设备中只有一个4个字节的全局变量int global_var,而这个设备的名字叫做"globalvar"。对"globalvar"设备的读写等操作即是对其中全局变量global_var的操作. 当globalvar模块被加载时,globalvar_init被执行,它将调用内核函数register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。

什么是dev_t

(39条消息) Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析_知秋一叶-CSDN博客_linux字符设备驱动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LDjROEu4-1639148931261)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=NzZkZWZkNGNkYmJjMDk2MDk4MWMyNzQzZjVjZWNiNWRfRjZ4cDA4MjNiYW9LMVhZdm02TVI5bGNaTHduYlk3VnhfVG9rZW46Ym94Y24xS1RGZXlCQWZxcDcyUFFUTnR4UTJjXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

如图,在Linux内核中:

a – 使用cdev结构体来描述字符设备;

b – 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

c – 通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

在Linux字符设备驱动中:

a – 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;

b – 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;

c – 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;

​ 用户空间访问该设备的程序:

什么是MKDEV

29.使用register_chrdev_region()系列来注册字符设备 - 诺谦 - 博客园 (cnblogs.com)

内核MKDEV(MAJOR, MINOR) - Lilto - 博客园 (cnblogs.com)

MKDEV(MAJOR, MINOR);

说明: 获取设备在设备表中的位置。

MAJOR 主设备号

MINOR 次设备号

MKDEV(ma,mi) 就是先将主设备号左移20位,然后与次设备号相加得到设备号。

什么是主设备号和次设备号

[主设备号和次设备号 - johnny_HITWH - 博客园 (cnblogs.com)](https://www.cnblogs.com/johnnyflute/p/3969774.html#:~:text=一个字符设备或者块设备都有一个主设备号和次设备号。. 主设备号和次设备号统称为设备号。. 主设备号用来表示一个特定的驱动程序。. 次设备号用来表示使用该驱动程序的各设备。. 例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。.,那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。. 这里,次设备号就分别表示两个LED灯。. 设备文件通常都在 /dev 目录下。. 如:.)

为了管理设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。

一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。

次设备号的主要用途:

1、区分设备驱动程序控制的实际设备;

2、区分不同用途的设备 (misc 系列设备);

3、区分块设备的分区 (partition)

通常,为了使应用程序区分所控制设备的类型,内核使用主设备号。而存在多台同类设备时,为了选择其中的一种,设备驱动程序就使用次设备号。

什么是静态分配设备号和动态分配设备号

[主设备号和次设备号 - johnny_HITWH - 博客园 (cnblogs.com)](https://www.cnblogs.com/johnnyflute/p/3969774.html#:~:text=一个字符设备或者块设备都有一个主设备号和次设备号。. 主设备号和次设备号统称为设备号。. 主设备号用来表示一个特定的驱动程序。. 次设备号用来表示使用该驱动程序的各设备。. 例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。.,那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。. 这里,次设备号就分别表示两个LED灯。. 设备文件通常都在 /dev 目录下。. 如:.)

(39条消息) register_chrdev_region()、register_chrdev()、 alloc_chrdev_region()函数的区别_博观而约取-CSDN博客_alloc_chrdev_region

静态分配设备号

静态分配设备号,就是驱动程序开发者,静态地指定一个设备号。对于一部分常用的设备,内核开发者已经为其分配了设备号。这些设备号可以在内核源码documentation/ devices.txt文件中找到。如果只有开发者自己使用这些设备驱动程序,那么其可以选择一个尚未使用的设备号。在不添加新硬件的时候,这种方式不会产生设备号冲突。但是当添加新硬件时,则很可能造成设备号冲突,影响设备的使用。

静态注册:

使用register_chrdev_region()首先需要定义一个dev_t变量来作为一个设备号,

dev_t   dev_num;

要想注册一个设备则需要一个主设备号。根据主设备号获取设备号:

dev_num=MKDEV(major,minor);

major是一个表示设备号的主设备号,minor次设备号

注册:

register_chrdev_region(dev_num,2,“dev_name”);

第一个参数表示设备号,第二个参数表示注册的此设备数目,第三个表示设备名称。

动态分配设备号

在Linux中有非常多的字符设备,在人为的为字符设备分配设备号时,很可能发生冲突。Linux内核开发者一直在努力将设备号变为动态的。可以使用alloc_chrdev_region()函数达到这个目的。(linux/fs.h)

动态注册:

如果我们提前知道设备的编号,那么就用register_chrdev_region(),但是如果我们不知道呢,我们就使用动态申请设备编号。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

  • dev:这个函数的第一个参数,是输出型参数,获得一个分配到的设备号。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是多少,方便我们在mknod创建设备文件时用到主设备号和次设备号。 mknod /dev/xxx c 主设备号 次设备号

  • baseminor:次设备号的基准,即从哪个次设备号开始分配。

  • count:次设备号的个数。

  • name:驱动的名字。

  • 返回值:小于0,则自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来。

根据设备号获取主设备号:

dev_major = MAJOR(dev_num);

释放设备号

使用上面两种方式申请的设备号,都应该在不使用设备时,释放设备号。设备号的释放统一使用下面的函数:

void unregister_chrdev_region(dev_t from, unsigned count);

在上面这个函数中,from表示要释放的设备号,count表示从from开始要释放的设备号个数。通常,在模块的卸载函数中调用unregister_chrdev_region()函数。

什么是cdev

一文搞懂内核中有关cdev的各种函数register_chrdev_region/alloc_chrdev_region/register_chrdev - 简书 (jianshu.com)

cdev:cdev是一个结构体,里面的成员来共同帮助我们注册驱动到内核中,表达字符设备的,将这个struct cdev结构体进行填充,主要填充的内容就是

struct cdev {struct kobject kobj;struct module *owner;//填充时,值要为 THIS_MODULE,表示模块const struct file_operations *ops;//这个file_operations结构体,注册驱动的关键,要填充成这个结构体变量struct list_head list;dev_t dev;//设备号,主设备号+次设备号unsigned int count;//次设备号个数
};

file_operations:将file_operations结构体变量的值赋给cdev中的ops成员后,这个结构体就会被cdev_add函数添加进内核。

涉及到cdev结构体的函数

  • cdev_alloc(cdev pcdev) :利用内核的kmalloc函数为这个结构体分配堆空间*,如果我们定义了一个全局的 static struct cdev *pcdev; 我们就可以用 pcdev = cdev_alloc();来给这个pcdev分配堆内存空间。

  • cdev_init(cdev , fops)

    :将struct cdev类型的结构体变量和file_operations结构体进行绑定。

    但若前面使用了cdev_alloc,则就可以直接利用

    pcdev->ops = fops;
    

    来进行绑定,就不需要cdev_init函数了。

    • 在cdev_init函数中,除了cdev->ops = fops;之外的其他的操作都在cdev_alloc函数中做了。
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{memset(cdev, 0, sizeof *cdev);INIT_LIST_HEAD(&cdev->list);cdev->kobj.ktype = &ktype_cdev_default;kobject_init(&cdev->kobj);cdev->ops = fops;
}
  • cdev_add:向内核里面添加一个驱动,注册驱动

Linux内核API cdev_add|极客笔记 (deepinout.com)

int cdev_add(struct cdev *, dev_t, unsigned)

cdev_add输入参数说明

函数 cdev_add()有三个输入参数,第一个输入参数代表即将被添加入Linux内核系统的字符设备,此结构体在极客笔记函数cdev_alloc()分析文档的返回参数说明部分有详细解释;第二个输入参数是dev_t类型的变量,此变量代表设备的设备号,其中包括主设备号和次设备号,其内核定义如下:

typedef __kernel_dev_t dev_t;

其中__kernel_dev_t的定义如下:

typedef __u32 __kernel_dev_t;

由此,可知dev_t其实是一个无符号的32位整数,其中32为的前12位代表主设备号,后20位代表此设备号。第三个输入参数是无符号的整型变量,代表想注册设备的设备号的范围,用于给struct cdev中的字段count赋值。

cdev_add返回参数说明

  • 函数cdev_add()返回int型的结果,表示设备是否添加成功。如果成功,则返回0,如果失败,则返回- ENOMEM, ENOMEM的被定义为12。

  • *****cdev_del(cdev pcdev):释放cdev结构体空间,cdev_del函数内部是能知道你的struct cdev定义的对象是用的堆内存还是栈内存还是数据段内存的。这个函数cdev_del调用时,会先去看你有没有使用堆内存,如果有用到的话,会先去释放掉你用的堆内存,然后在注销掉你这个设备驱动。所以,如果struct cdev要用堆内存一定要用内核提供的这个cdev_alloc去分配堆内存,因为内部会做记录,这样在cdev_del注销掉这个驱动的时候,才会去释放掉那段堆内存。

什么是printk

(39条消息) printk函数的用法_wwwlyj123321的博客-CSDN博客_printk

printk在内核源码中用来记录日志信息的函数,只能在内核源码范围内使用。用法和printf非常相似。printk函数主要做两件事情:第一件就是将信息记录到log中,而第二件事就是调用控制台驱动来将信息输出

什么是try_module_get

[try_module_get和module_put【转】 - sky-heaven - 博客园 (cnblogs.com)](https://www.cnblogs.com/sky-heaven/p/5569236.html#:~:text=int try_module_get (struct module *module); 用于增加模块使用计数;若返回为0,表示调用失败,希望使用的模块没有被加载或正在被卸载中。 void,module_put (struct module *module); 减少模块使用计数。 try_module_get与module_put 的引入与使用与2.6内核下的设备模型密切相关。)

int try_module_get(struct module *module); 用于增加模块使用计数;若返回为0,表示调用失败,希望使用的模块没有被加载或正在被卸载中。

(39条消息) try_module_get和module_put_jk110333的专栏-CSDN博客

位置: /linux/kernel/module.c

声明static inline int try_module_get(structmodule *module)

功能:判断 module 模块是否处于活动状态,然后通过 local_inc() 宏将该模块的引用计数加 1

返回值

linux-2.6中返回值是一个整数,如果该模块处于活动状态且对它引用计数加1操作正确则返回1,否则返回0.

linux-3.7.5中返回值是一个bool量,正确返回true,错误返回false!

//LINUX2.6
static inline int try_module_get(struct module *module)
{int ret = 1;if (module) {unsigned int cpu = get_cpu();if (likely(module_is_live(module))) {local_inc(__module_ref_addr(module, cpu));trace_module_get(module, _THIS_IP_,local_read(__module_ref_addr(module, cpu)));}   elseret = 0;put_cpu();}   return ret;
}
//LINUX3.75
bool try_module_get(struct module *module)
{bool ret = true;if (module) {preempt_disable();if (likely(module_is_live(module))) {__this_cpu_inc(module->refptr->incs);trace_module_get(module, _RET_IP_);} else ret = false;preempt_enable();}    return ret;
}
EXPORT_SYMBOL(try_module_get);

什么是module_put

声明

Linux-3.7.5中void module_put(struct module *module)

Linux-2.6中static inline void module_put(struct module *module)

功能:使指定的模块使用量减一

什么是wait_event_interruptible

(39条消息) wait_event_interruptible 使用方法_allen6268198的专栏-CSDN博客_wait_event_interruptible

wait_event_interruptible(9) — linux-manual-3.16 — Debian jessie — Debian Manpages

NAME

wait_event_interruptible - sleep until a condition gets true

SYNOPSIS

wait_event_interruptible(wq, condition);

ARGUMENTS

wq

the waitqueue to wait on

condition

a C expression for the event to wait for

DESCRIPTION

The process is put to sleep (TASK_INTERRUPTIBLE) until the condition evaluates to true or a signal is received. The condition is checked each time the waitqueue wq is woken up.

wake_up has to be called after changing any variable that could change the result of the wait condition.

The function will return -ERESTARTSYS if it was interrupted by a signal and 0 if condition evaluated to true.

什么是copy_to_user, copy_from_user

(39条消息) copy_to_user和copy_from_user两个函数的分析_杨德龙的专栏-CSDN博客_copy_to_user

由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到用户空间的复制。如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。

什么是up()

(39条消息) Linux内核信号量-up()和down()_Jay14的博客-CSDN博客_信号量up

当进程希望释放内核信号量锁时,就调用up()函数。

什么是down_interruptible

[Linux内核API down_interruptible|极客笔记 (deepinout.com)](https://deepinout.com/linux-kernel-api/linux-kernel-api-synchronization-mechanism/linux-kernel-api-down_interruptible.html#:~:text=down_interruptible (),函数用来获取信号量,将信号量sem的计数器值减1,但它是可被信号中断的,这一点与 down ()函数不同。)

**down_interruptible()**函数用来获取信号量,将信号量sem的计数器值减1,但它是可被信号中断的,这一点与down()函数不同。当有另外的内核控制路径给这个因为竞争不到信号量而睡眠的进程发送了一个信号时,它收到信号后就会立即返回,而放弃继续获得信号量。

down_interruptible输入参数说明

  • sem:信号量结构体指针,指向将要获取的信号量。其中,关于信号量结构体semaphore的说明参考极客笔记的sema_init()函数的分析说明。

down_interruptible返回参数说明

  • down_interruptible()函数返回一个整型值,如果成功获取了信号量,则返回0,否则在收到中断信号后,将返回-EINTR。

什么是register_chrdev_region,unregister_chrdev_region

前面用register_chrdev_region静态注册了设备号或用alloc_chrdev_region动态分配了设备号,这里用unregister_chrdev_region来注销设备号。

报错insmod: ERROR: could not insert module globalvar.ko: Operation not permitted

需要在root权限下运行。

sudo insmod globalvar.ko

报错:./read或./write打不开文件

需要在root权限下运行。

sudo ./read
sudo ./write

读写程序的问题

使用如下读写程序,写入信息时有时新窗口能读出来,有时新窗口读不出来。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
{int fd,i; char num[101]; //打开设备fd = open("/dev/chardev0", O_RDWR, S_IRUSR | S_IWUSR);if (fd != -1 ){int pid1,pid2;while((pid1=fork())==-1);if(pid1>0){//父进程while((pid2=fork())==-1);if(pid2>0)//父进程{wait(0);wait(0);return 0;}else{//子进程2while(1){for(i=0;i<101;i++) num[i]='\0'; read(fd,num,100); printf("The chardev0 is:%s\n",num); if(strcmp(num,"quit")==0) { close(fd); break; }}}}else{//子进程1//写chardev0while(1){printf("Please input the globalvar:\n"); scanf("%s",num); write(fd,num,strlen(num)); if(strcmp(num,"quit")==0) { close(fd); break; }}} }else{printf("Device open failure\n");}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-goh1Ql7w-1639148931262)(https://xz2k2i3v0u.feishu.cn/space/api/box/stream/download/asynccode/?code=MTY3YjcxNjdmY2U4ZDI4YzQ5YmZiMmQwZjliNTY0ODNfU1ljbFVGRnRNajRxWXRwN1VzdnI2dUJVemExOVpobmNfVG9rZW46Ym94Y245WTFFZDF1VTN4UmZtVmZmZHlrS1ZlXzE2MzkxNDg5MjA6MTYzOTE1MjUyMF9WNA)]

子进程2中循环执行for(i=0;i<101;i++) num[i]=’\0’;使得有时缓冲区充满了\0 。应将程序改为

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
{int fd,i; char num[101]; //打开设备fd = open("/dev/chardev0", O_RDWR, S_IRUSR | S_IWUSR);if (fd != -1 ){int pid1,pid2;while((pid1=fork())==-1);if(pid1>0){//父进程while((pid2=fork())==-1);if(pid2>0)//父进程{wait(0);wait(0);return 0;}else{for(i=0;i<101;i++) num[i]='\0';//子进程2while(1){ read(fd,num,100); printf("The chardev0 is:%s\n",num); if(strcmp(num,"quit")==0) { close(fd); break; }}}}else{//子进程1//写chardev0while(1){printf("Please input the globalvar:\n"); scanf("%s",num); write(fd,num,strlen(num)); if(strcmp(num,"quit")==0) { close(fd); break; }}} }else{printf("Device open failure\n");}
}

改后还是不对,尝试使用pthread。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NUMBER_OF_THREADS 2
void* read_thread(void* tid);
void* write_thread(void* tid);
int fd,i;
char num[101];
int main(void){fd = open("/dev/chardev0", O_RDWR, S_IRUSR | S_IWUSR);if (fd != -1 ){pthread_t threads[NUMBER_OF_THREADS];//tid线程标识符int status,i;printf("主函数中创建读线程\n");//创建线程,线程函数传入参数为istatus=pthread_create(&threads[0],NULL,read_thread,(void*)0);if(status!=0){//线程创建不成功,打印错误信息printf("线程创建失败 %d\n",status);exit(-1);}printf("主函数中创建写线程\n");//创建线程,线程函数传入参数为istatus=pthread_create(&threads[1],NULL,write_thread,(void*)1);if(status!=0){//线程创建不成功,打印错误信息printf("线程创建失败 %d\n",status);exit(-1);}for(i=0;i<NUMBER_OF_THREADS;i++){pthread_join(threads[i],NULL);}exit(0);}else{printf("Device open failure\n");}}
void* read_thread(void* tid){int i;for(i=0;i<101;i++) num[i]='\0';while(1){ read(fd,num,100); printf("The chardev0 is:%s\n",num); if(strcmp(num,"quit")==0) { close(fd); break; }}pthread_exit(0);
}
void* write_thread(void* tid){while(1){printf("Please input the globalvar:\n"); scanf("%s",num); write(fd,num,strlen(num)); if(strcmp(num,"quit")==0) { close(fd); break; }}
}

这样仍然会有上述问题,发现是因为缓冲区只能读一次,一个窗口读后读指针就改变位置了,因此要控制写的窗口不能同时读。

尝试在某一进程写入后休眠0.1s,这样就会让另一个进程读先完。代码如下

#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
{int fd,i; char num[101];int fd2[2]; char InPipe;               // 定义读缓冲区char c1='1',c2='2'; pipe(fd2);//创建管道//打开设备fd = open("/dev/chardev0", O_RDWR, S_IRUSR | S_IWUSR);if (fd != -1 ){int pid1,pid2;while((pid1=fork())==-1);if(pid1>0){//父进程while((pid2=fork())==-1);if(pid2>0)//父进程{wait(0);wait(0);return 0;}else{for(i=0;i<101;i++) num[i]='\0';//子进程2while(1){ lockf(fd2[0],1,0);read(fd2[0],InPipe,1);lockf(fd2[0],1,0);if(InPipe==c1)//另一个窗口正在读,不允许读continue;else{read(fd,num,100); printf("The chardev0 is:%s\n",num); if(strcmp(num,"quit")==0) { close(fd); break; }}}}}else{//子进程1//写chardev0while(1){printf("Please input the globalvar:\n"); scanf("%s",num); //传递正在写信息lockf(fd2[1],1,0);write(fd2[1],&c1,1);lockf(fd2[1],0,0);//写数据write(fd,num,strlen(num)); if(strcmp(num,"quit")==0) { close(fd); break; }//等另一个窗口读完sleep(0.1);lockf(fd2[1],1,0);write(fd2[1],&c2,1);lockf(fd2[1],0,0);}} }else{printf("Device open failure\n");}
}

但是还是有问题,还在寻求解决方法。

有一个办法是不用子进程,每一次循环中如果键盘有输入则调用写,没有则调用读。(失败)

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h> int kbhit(void)
{ struct termios oldt, newt; int ch; int oldf; tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt); oldf = fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); ch = getchar(); tcsetattr(STDIN_FILENO, TCSANOW, &oldt); fcntl(STDIN_FILENO, F_SETFL, oldf); if(ch != EOF) { ungetc(ch, stdin); return 1; } return 0;
}
main()
{int fd,i; char num[101];//打开设备fd = open("/dev/chardev0", O_RDWR, S_IRUSR | S_IWUSR);if (fd != -1 ){for(i=0;i<101;i++) num[i]='\0';while(1){printf("若要输入内容,请按W,若要读取内容,请按R。\n");if(kbhit()){printf("请输入内容。\n");scanf("%s",num); write(fd,num,strlen(num)); if(strcmp(num,"quit")==0) { close(fd); break; } }else{ read(fd,num,100); printf("%s\n",num); if(strcmp(num,"quit")==0) { close(fd); break; }}}}else{printf("Device open failure\n");}
}

beifen

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <memory.h>
#include <unistd.h>
#include <termios.h>int kbhit(void);int main()
{int fd,i;char msg[101];fd=open("/dev/chardev0", O_RDWR|O_APPEND);if(fd==-1){fprintf(stderr, "can't openfile chardev0\n");exit(0);}while(1){if( kbhit() ) {char k[100]; //用来存储键盘输入的数据char head[150]; //存放消息头信息和数据,表明是哪个进程写入的消息int pid = getpid();fgets(k,100,stdin);sprintf(head,"进程%d:%s",pid,k);write(fd,head,strlen(head));//写入字符设备}for(i=0;i<101;i++)msg[i]='\0';read(fd,msg,100);if(strlen(msg) != 0 ) { //要有新数据才会显示在屏幕上printf("%s\n",msg);         }sleep(1);}close(fd);return 0;
}//非阻塞检测按键函数
int kbhit(void)
{ struct termios oldt, newt;int ch,oldf; tcgetattr(STDIN_FILENO, &oldt);newt=oldt;newt.c_lflag &= ~(ICANON | ECHO);tcsetattr(STDIN_FILENO, TCSANOW, &newt);oldf=fcntl(STDIN_FILENO, F_GETFL, 0);fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);ch=getchar();tcsetattr(STDIN_FILENO, TCSANOW, &oldt);fcntl(STDIN_FILENO, F_SETFL, oldf);if(ch != EOF) { ungetc(ch, stdin);return 1;} return 0;
}{printf("Device open failure\n");}
}

beifen

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <memory.h>
#include <unistd.h>
#include <termios.h>int kbhit(void);int main()
{int fd,i;char msg[101];fd=open("/dev/chardev0", O_RDWR|O_APPEND);if(fd==-1){fprintf(stderr, "can't openfile chardev0\n");exit(0);}while(1){if( kbhit() ) {char k[100]; //用来存储键盘输入的数据char head[150]; //存放消息头信息和数据,表明是哪个进程写入的消息int pid = getpid();fgets(k,100,stdin);sprintf(head,"进程%d:%s",pid,k);write(fd,head,strlen(head));//写入字符设备}for(i=0;i<101;i++)msg[i]='\0';read(fd,msg,100);if(strlen(msg) != 0 ) { //要有新数据才会显示在屏幕上printf("%s\n",msg);         }sleep(1);}close(fd);return 0;
}//非阻塞检测按键函数
int kbhit(void)
{ struct termios oldt, newt;int ch,oldf; tcgetattr(STDIN_FILENO, &oldt);newt=oldt;newt.c_lflag &= ~(ICANON | ECHO);tcsetattr(STDIN_FILENO, TCSANOW, &newt);oldf=fcntl(STDIN_FILENO, F_GETFL, 0);fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);ch=getchar();tcsetattr(STDIN_FILENO, TCSANOW, &oldt);fcntl(STDIN_FILENO, F_SETFL, oldf);if(ch != EOF) { ungetc(ch, stdin);return 1;} return 0;
}

操作系统实验·字符设备驱动程序相关推荐

  1. 经典 【操作系统实验】 实验六 设备驱动程序 RH5 2.6.18 + 2.6.32 内核

    经典 [操作系统实验] 实验六 设备驱动程序 设备驱动程序 简单介绍一下2.6版本内核添加模块的方法: 虚拟块设备驱动程序内容 设备驱动程序 前言: 本文是基于Linux的设备驱动实验流程记录,涵盖了 ...

  2. 字符设备驱动程序——点亮、熄灭LED操作

    2019独角兽企业重金招聘Python工程师标准>>> 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流的设 ...

  3. Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?

    作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...

  4. 字符设备驱动程序框架

    1, 设备号的内部表示形式 类型:dev_t 32=12(主设备号) + 20(次设备号) 相关宏:<linux/kdev_t.h> MAJOR(dev_t dev) MINOR(dev_ ...

  5. 字符设备驱动程序的传统写法

    以led驱动程序为例,介绍字符设备驱动程序的传统写法. 驱动程序: 程序代码来源于韦老大视频代码 1 #include <linux/module.h> 2 #include <li ...

  6. 第12课第3节 字符设备驱动程序之查询方式的按键驱动程序

    第12课第3节 字符设备驱动程序之查询方式的按键驱动程序 cat /proc/devices //查询主设备号 insmod ./second_drv.ko ls /dev/button -l pos ...

  7. 字符设备驱动0:一个简单但完整的字符设备驱动程序

    参考: linux设备驱动程序之简单字符设备驱动 [很详细,必看]http://www.cnblogs.com/geneil/archive/2011/12/03/2272869.html //在驱动 ...

  8. 第12课第2.2节 字符设备驱动程序之LED驱动程序_测试改进

    第12课第2.2节 字符设备驱动程序之LED驱动程序_测试改进 //仅用flash上的根文件系统启动后,手工MOUNT NFS mount -t nfs -o nolock,vers=2 192.16 ...

  9. 字符设备驱动程序的使用

    1.编译.安装驱动 linux系统中,驱动程序通常采用内核模块的程序结构来进行编码,因此,编译.安装一个驱动程序,其实质就是编译.安装一个内核模块. 将文件memdev.c makefile 放入虚拟 ...

  10. i.MX6ULL学习笔记--字符设备驱动程序

    i.MX6ULL学习笔记--字符设备驱动程序 简介 1.驱动的配置过程 1.1设备号 1.2哈希表-chrdevs 1.3哈希表-obj_map->probes 1.4文件操作接口 1.5简单了 ...

最新文章

  1. 超详细的Guava RateLimiter限流原理解析
  2. 学python需要什么文化基础-人工智能对人类有哪些影响 选择Python入门怎样
  3. 组织可以最大限度提高数据中心性能的五个步骤
  4. 文件被后台程序占用无法删除_win10重装后系统占用50G?只要做好这2步,运行比win7还快...
  5. css类选择器或逻辑,深入理解CSS中选择器的逻辑处理
  6. LetCode-算法-整数反转
  7. poj1958 Strange Towers of Hanoi 题解报告
  8. nginx-正则表达式-重定向
  9. 解决firefox的button按钮文字不能垂直居中
  10. 文字处理技术:试图通过多次布局解决布局问题的思路是否可以避免?
  11. Ubuntu16.04刷机+装驱动
  12. 计算机单片机毕设答辩问题,单片机毕业论文答辩常见问题.docx
  13. 马斯克是全人类的?他旗下有9家公司,特斯拉被评为最没技术含量
  14. 成就:优秀的管理者成就自己,卓越的管理者成就他人(读后感)
  15. https://acs.jxnu.edu.cn/problem/GYM103495E
  16. Codeforces 743 D Chloe and pleasant prizes
  17. python学习之面向对象(二)
  18. IoT物联网——各大厂质量保障实践分享汇总(智能语音视频篇)
  19. CREO2——解决CREO生成的二维图drw转换成CAD的dwg格式尺寸失真问题
  20. 原生 js 获取所有兄弟节点

热门文章

  1. linux中高危端口,关闭高危端口方法[转载]
  2. python基础语法记录
  3. 8uftp链接linux,8UFTP工具,FTP工具连接的办法,配置方式
  4. 利用Java寻找完美数
  5. 【Typecho插件-前端-播放器】BiliVid -- 好用的Bilibili视频链接解析播放器
  6. SpringMVC-视图和视图解析器
  7. 手工笔筒制作教程(附彩色贴图分享)
  8. 2021年4月中国旅游行业网络关注度分析报告
  9. python制作adobe photoshop插件_Adobe Ps 2021已上线,新功能秒杀一切插件
  10. yuv444转yuv422 matlab,最简单解释 YUV444,YUV422,YUV420中的4,2,0