Unix/Linux操作系统分析实验一 进程控制与进程互斥
Unix/Linux操作系统分析实验二 内存分配与回收:Linux系统下利用链表实现动态内存分配
Unix/Linux操作系统分析实验三 文件操作算法: 实现在/proc目录下添加文件
本文章用于记录自己所学的内容,方便自己回顾复习。
实验内容
内容一:
编写一个字符设备驱动,并利用对字符设备的同步操作,设计实现一个聊天程序。可以有一个读,一个写进程共享该字符设备,进行聊天;也可以由多个读和多个写进程共享该字符设备,进行聊天。
内容二: 生产者-消费者并发实例的实现机制(教材P191)
随着人们生活水平的提高,每天早餐基本是牛奶、面包。而在牛奶生产的环节中,生产厂家必须和经销商保持良好的沟通才能使效益最大化,具体说就是生产一批就卖一批,并且只有卖完了,才能生产下一批,这样才能达到供需平衡,否则就有可能造成浪费(供过于求)或者物资短缺(供不应求)。假设现在有一个牛奶生产厂家,它有一个经销商,并且由于资金不足,只有一个仓库。牛奶生产厂家首先生产一批牛奶,并放在仓库里,然后通知经销商来批发。经销商卖完牛奶后,打电话再订购下一批牛奶。牛奶生产厂家接到订单后,才开始生产下一批牛奶。
上述问题中,牛奶生产厂家就相当于“生产者”,经销商为“消费者”,仓库则为“公共缓冲区”。问题属于单一生产者,单一消费者,单一公共缓冲区。这属于典型的进程同步问题。生产者和消费者为不同的线程,“公共缓冲区”则为临界区。在同一时刻,只能有一个线程访问临界区。实现上述问题的生产者-消费者并发实例。
实验步骤
内容一:编写一个字符设备驱动
1、编写字符设备驱动函数、模块的初始化函数和退出/清理函数;
2、make——编译模块;
3、sudo insmod globalvar.ko——加载模块;
4、gcc -o reader reader.c——编译reader文件;
5、gcc -o writer writer.c——编译writer文件;
6、sudo ./writer——运行writer程序;
7、sudo ./reader——运行reader程序;
8、sudo dmesg——观察结果;
9、sudo rmmod globalvar——卸载模块。
内容二: 生产者-消费者并发实例的实现机制(教材P191)
1、编写生产者线程、消费者线程、模块的初始化函数和退出/清理函数;
2、make——编译模块;
3、sudo insmod procon.ko——加载模块;
4、sudo dmesg——观察结果;
5、sudo rmmod procon——卸载模块。
实验结果分析(截屏的实验结果,与实验结果对应的实验分析)
实验结果:
内容一:
内容二:
实验分析:
从结果可以看出,生产者线程首先执行生产一批产品,然后等待消费者线程消耗产品。只有消费者消费后,生产者才能再进行生产。生产者严格按照生产顺序生产,消费者也严格按照消费顺序生产。
实验总结
通过这次实验,我学会如何访问字符设备,如何分配和释放设备号,理解字符设备驱动的组成;学会如何使用内核同步方法去避免并发,防止竞争;理解自旋锁、信号量在并发时的作用和它们之间的区别,最后实现了简单的生产者—消费者并发实例。
实验中出现的问题
1、内容二:生产者-消费者并发实验中,出现如下图错误:
查阅博客得知:在 2.6.37 之后的 Linux 内核中, init_mutex 已经被废除了, 新版本使用 sema_init 函数。
解决方法:
2、内容二:生产者-消费者并发实验中,出现如下图错误:
查阅博客得知:在Linux2.6版本之前该函数可以被驱动模块调用,因为被EXPORT_SYMBOL(kernel_thread);,但是在版本5.4.0没有没export,因此最好只用kthread_create()/kthread_run()来创建内核线程。
解决方法:
所有实验的源代码如下:
globalvar.c
//深入浅出linux设备驱动开发
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/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 dev{
struct cdev devm; //字符设备
struct semaphore sem;
wait_queue_head_t outq; //等待队列,实现阻塞操作
int flag; //阻塞唤醒标志
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 *filp, char *buf, size_t len, loff_t *off) {
//参数 filp 是文件指针,参数 count 是请求传输的数据长度,参数 buff 是指向用户空间的缓冲区,这个缓冲区或者保存将写入的数据,或者是一个存放新读入数据的空缓冲区,最后的 offp 是一个指向“long offset type(长偏移量类型)”对象的指针,这个对象指明用户在文件中进行存取操作的位置。返回值是“signed size type(有符号的尺寸类型)”
if(wait_event_interruptible(globalvar.outq, globalvar.flag != 0)) //不可读时 阻塞读进程
return -ERESTARTSYS;
//函数down_interruptible()返回0表示中断成功,否则出错。
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 %ld\n",len);
if(raw_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 %ld\n",len);
if(raw_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 %ld\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 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);
}
//结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。设备"gobalvar"的基本入口点结构变量gobalvar_fops。
struct file_operations globalvar_fops = {
//用来从设备中获取数据. 在这个位置的一个空指针导致 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,
};
static int globalvar_init(void) {
//内核模块的初始化
int result = 0;
int err = 0;
dev_t dev = MKDEV(major, 0); // 成功执行返回dev_t类型的设备编号,dev_t类型是unsigned int 类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号,可以通过MAJOR和MINOR来获得主设备号和次设备号。
if(major)
result = register_chrdev_region(dev, 1, "charmem"); //静态申请设备编号
else {
result = alloc_chrdev_region(&dev, 0, 1, "charmem"); //动态分配设备号
major = MAJOR(dev);
}
if(result < 0)
return result;
cdev_init(&globalvar.devm, &globalvar_fops); //注册字符设备驱动,设备号和file_operations结构体进行绑定
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
}
my_class = class_create(THIS_MODULE, "chardev0");
device_create(my_class, NULL, dev, NULL, "chardev0");
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);//注销设备
}
module_init(globalvar_init);
module_exit(globalvar_exit);
MODULE_LICENSE("GPL");
Makefile
ifneq ($(KERNELRELEASE),)
obj-m := globalvar.o#obj-m 指编译成外部模块
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
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;
}
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;
}
procon.c
#include <linux/kthread.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/semaphore.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <asm/atomic.h>
#define PRODUCT_NUMS 10
static struct semaphore sem_producer;
static struct semaphore sem_consumer;
static char product[12];
static atomic_t num;
static int producer(void* product);
static int consumer(void* product);
static int id = 1;
static int consume_num = 1;
//生产者线程
static int producer(void* p)
{
char* product = (char*)p;
int i;
atomic_inc(&num);
printk("producer[%d] start...\n", current->pid);
for (i = 0; i < PRODUCT_NUMS; i++) {
down(&sem_producer);
snprintf(product, 12, "2021-10-%d", id++);
printk("producer[%d] produce %s\n", current->pid, product);
up(&sem_consumer);
}
printk("producer[%d] exit...\n", current->pid);
return 0;
}
//消费者线程
static int consumer(void* p)
{
char* product = (char*)p;
printk("consumer[%d] start...\n", current->pid);
for (;;) {
msleep(100);
down_interruptible(&sem_consumer);
if (consume_num >= PRODUCT_NUMS * atomic_read(&num))
break;
printk("consumer[%d] consume %s\n", current->pid, product);
consume_num++;
memset(product, '\0', 12);
up(&sem_producer);
}
printk("consumer[%d] exit...\n", current->pid);
return 0;
}
//模块插入和删除
static int procon_init(void)
{
printk(KERN_INFO"show producer and consumer\n");
//在 2.6.37 之后的 Linux 内核中, init_mutex 已经被废除了, 新版本使用 sema_init 函数.
//init_MUTEX(&sem_consumer);
sema_init(&sem_producer, 1);
//init_MUTEX_LOCKED(&sem_consumer);
sema_init(&sem_consumer, 0);
atomic_set(&num, 0);
//kernel_thread函数的作用是产生一个新的线程。内核线程实际上就是一个共享父进程地址空间的进程,它有自己的系统堆栈。内核线程和进程都是通过do_fork()函数来产生的。
//在Linux2.6版本之前该函数可以被驱动模块调用,因为被EXPORT_SYMBOL(kernel_thread);,但是在版本5.4.0没有没export,因此最好只用kthread_create()/kthread_run()来创建内核线程。
//kernel_thread(producer, product, CLONE_KERNEL);
kthread_run(producer, product, "propro");
//kernel_thread(consumer, product, CLONE_KERNEL);
kthread_run(consumer, product, "conpro");
return 0;
}
static void procon_exit(void)
{
printk(KERN_INFO"exit producer and consumer\n");
}
module_init(procon_init);
module_exit(procon_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("producer and consumer Module");
MODULE_ALIAS("a simplest module");
Makefile
obj-m+=procon.o #产生procon模块的目标文件
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
#shell uname -r: Linux内核源代码的当前版本
#PWD: 模块所在的当前路径