代码编织梦想

哈工大操作系统实验——信号量

实验入口
https://www.lanqiao.cn/courses/115
主要参考文章
https://www.dotcpp.com/course/461  lseek()函数:用于移动打开文件的指针
https://cloud.tencent.com/developer/article/1425052 linux系统调用之write源码解析(基于linux0.11)
https://blog.csdn.net/yuanren201/article/details/103207910 get_fs_bytes解析
https://blog.csdn.net/to_free/article/details/115187981 VIM与系统剪贴板的复制粘贴
https://zhuanlan.zhihu.com/p/428283092 操作系统实验六 信号量的实现和应用(哈工大李治军)
https://blog.csdn.net/weixin_43987915/article/details/108949942 哈工大操作系统实验6 信号量的实现 pc.c 编译时报错 对‘sem_open‘未定义的引用
https://blog.csdn.net/qq_59804059/article/details/120634348 Linux 文件编程 open函数
https://blog.csdn.net/qq_42518941/article/details/119757885 哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用

任务一 实现pc.c

在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能:

1.建立一个生产者进程,N 个消费者进程(N>1);
2.用文件建立一个共享缓冲区;
3.生产者进程依次向缓冲区写入整数 0,1,2,...,M,M>=500;
4.消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和 + 数字输出到标准输出;
5.缓冲区同时最多只能保存 10 个数。
其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。

先附上我的代码吧【注:我没做到从缓冲区删除,但其他都完成了】

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <semaphore.h>

const char* filename="buffer.txt";

sem_t* empty;
sem_t* full;
sem_t* mutex;

int fd;

void Producer(){
	int i;
	char s[5]={0};
	for(i=0;i<=500;i++){
		sem_wait(empty);
		sem_wait(mutex);
		int tmp=lseek(fd,0,SEEK_CUR);
		lseek(fd,0,SEEK_END);
		sprintf(s,"%03d\n",i);
		write(fd,s,4);
		lseek(fd,tmp,SEEK_SET);
		sem_post(mutex);
		sem_post(full);
	}
}

void Consumer(){
	sem_wait(full);
	sem_wait(mutex);
	char s[5]={0};
	read(fd,s,4);
	printf("%d : %s",getpid(),s);
	sem_post(mutex);
	sem_post(empty);
}

int main(){
	sem_unlink("empty");
	sem_unlink("full");
	sem_unlink("mutex");
	fd=open(filename,O_RDWR|O_CREAT);
	printf("%d\n",errno);	
	empty=sem_open("empty",O_CREAT,0644,10);
	full=sem_open("full",O_CREAT,0644,0);
	mutex=sem_open("mutex",O_CREAT,0644,1);
	
	if(!fork()){
		Producer();
		return 0;
	}
	
	int i;
	for(i=0;i<10;i++){
		if(!fork()){
			while(1)	Consumer();
		}
	}
	close(fd);
	return 0;
}

要点1 系统调用的IO读写

这部分耗费了我海量时间,主要原因还是因为我没有好好学就直接上手写导致很多地方都因为不清楚而寄了。。。

先大致讲讲文件读写的原理吧。打开一个文件作为数据流,有一个文件指针,该指针指向的地方就是之后读写开始的地方,读写还有lseek都可以让指针移动。

再放个各个系统调用的签名。

@param 文件名 模式

@return 所需文件描述符

int open(char* filename,int flag);

其中flag的可能取值:

在这里插入图片描述在这里插入图片描述如果想要多个方式并行,则可以用|连接。【联系一下原理,这大概是用了标志位吧,每个标志只有一位是1】

这部分踩过的坑:

① 选择O_CREAT,如果文件已经存在,居然是会报错?【表现为errno=13,还会输出一堆奇怪的东西】

@param 文件描述符  写入字符串  写入长度
@return 报错信息
int read(int fd,char* string,size_t size);

read会读出size个字节然后存进string里面,同时也会移动文件指针向前size个字节。

@param 文件描述符  写入字符串  写入长度
@return 报错信息
int write(int fd,char* string,size_t size);

基本同write。

这部分踩过的坑:

write(fd,NULL,0) ——合法

write(fd,NULL,a),a>0 ——寄!

这还是因为write的具体实现了。

write里面有个判断
在这里插入图片描述
而get_fs_byte:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EaijfViq-1664543018115)(C:\Users\17765\AppData\Roaming\Typora\typora-user-images\image-20220928204140436.png)]
确实感觉空的话挺危险的【】

@param 文件描述符
@return 报错信息
int close(fd);

这个没啥好说的,记得关就是了

要点2 信号量的调用

这方面看linux自带的man文档就行,写得很清楚。

输入指令:

man sem_overview

这部分踩过的坑:

千万注意最后不使用信号量时要释放,使用sem_unlink。不然最后的输出结果会非常诡异。

要点3 编写程序

以上差不多就是涉及到的需要自己了解的课外知识点了,接下来就需要自己编写程序。

总体框架就按它给的差不多:

Producer()
{
    // 空闲缓存资源
    P(Empty);

    // 互斥信号量
    P(Mutex);

    //生产并放一个item进缓冲区
    
    V(Mutex);
    V(Full);
}

Consumer()
{
    P(Full);
    P(Mutex);

    //从缓存区取出一个赋值给item并消费;
    
    V(Mutex);
    V(Empty);
}

有个点挺有趣的,就是它实际上把文件指针也看成一种资源了,因此也需要在同步段对其进行更新。

printf的stdout也是资源。

故以上两者都只能在锁内同步段进行更新。

main函数就照本宣科地用fork建立子进程就行。

实验二 自己实现信号量

Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:
sem_t *sem_open(const char *name, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_unlink(const char *name);

sem_open() 的功能是创建一个信号量,或打开一个已经存在的信号量。
sem_t 是信号量类型,根据实现的需要自定义。
name 是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。如果该信号量不存在,就创建新的名为 name 的信号量;如果存在,就打开已经存在的名为 name 的信号量。
value 是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID 等),由另两个系统调用使用。如失败,返回值是 NULL。
sem_wait() 就是信号量的 P 原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。返回 0 表示成功,返回 -1 表示失败。
sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。
sem_unlink() 的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。
在 kernel 目录下新建 sem.c 文件实现如上功能。然后将 pc.c 从 Ubuntu 移植到 0.11 下,测试自己实现的信号量。

由于不小心写完的实验代码被销毁了,因此差不多参考的是这篇文章【戳这里】,修改了一些地方,构成了我的回忆版代码。

目前未经测试。

要点1 系统调用修改

详见文章,写得很清楚。

要点2 sem.c文件的编写

sem_t定义

/* 定义的信号量数据结构: */
# ifndef _SEM_H_
# define _SEM_H_

#include<linux/sched.h>

typedef struct semaphore_t 
{
	char name[20];/* 信号量的名称 */
	int value;    /* 信号量的值 */
    int active;//我自己加的,是对象池思想,感觉写得还挺好的2333
	struct tast_struct *queue;/* 指向阻塞队列的指针 */
} sem_t;

#endif
#include <unistd.h> 
#include <string.h> 
#include <linux/sem.h> 
#include <asm/segment.h> 
#include <asm/system.h>

#define SEM_LIST_LENGTH 50

//信号量表
sem_t sem_list[SEM_LIST_LENGTH];

sem_open

/*
sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量。
*/

sem_t *sys_sem_open(const char * name,unsigned int value)
{
	if (name == NULL)
    {
        errno = 1;
        return NULL;
    }
    /* 首先将信号量的名称赋值到新建的缓冲区中 */
    char nbuf[20];
    int i;
    for(i = 0; i< 20; i++)
    {
    	nbuf[i] = get_fs_byte(name+i);
    }

    /* 然后开始遍历已有的信号量数组,如果有该名字的信号量,直接返回信号量的地址 */
    for(i = 0; i < SEM_LIST_LENGTH; i++)
    {
        if(sem_list[i].active==1&&!strcmp(sem_list[i].name, nbuf))
        {
            return &sem_list[i];
        }
    }
    /* 如果找不到信号量,就开始新建一个名字为name的信号量 */
    for(i = 0; i < SEM_LIST_LENGTH; i++)
    {
        if(sem_list[i].active==0)
        {
            strcpy(sem_list[i].name, nbuf);
    		sem_list[i].value = value;
            sem_list[i].active=1;
   			sem_list[i].queue = NULL;
            return &sem_list[i];
        }
    }
    
    //表已满
    errno = 1;
    return NULL;
}

sem_wait

/*
 sem_wait()就是信号量的P原子操作。
 如果继续运行的条件不满足,则令调用进程等待在信号量sem上。
 返回0表示成功,返回-1表示失败。
 */
int sys_sem_wait(sem_t * sem)
{
    /* 判断:如果传入的信号量是无效信号量,P操作失败,返回-1 */
    if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
    {
        errno=1;
        return -1;
    }
    /* 关中断 */
    cli();
    while(sem->value < 0)
    {
        sleep_on(&(sem->queue));
    }
    sem->value--; 
    /* 开中断 */
    sti();
    return 0;
}

sem_post

/*
sem_post()就是信号量的V原子操作。
如果有等待sem的进程,它会唤醒其中的一个。
返回0表示成功,返回-1表示失败。
*/
int sys_sem_post(sem_t * sem)
{
    /* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
    if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
    {
        return -1;
    }
    /* 关中断 */
    cli();
    sem->value++;
    /* 如果有等待sem的进程,它会唤醒其中的一个。 */
    if(sem->value <= 0)
    {
    	wake_up(&(sem->queue));
    }
    /* 开中断 */
    sti();
    return 0;
}

sem_unlink

/*
sem_unlink()的功能是删除名为name的信号量。
返回0表示成功,返回-1表示失败。
*/
int sys_sem_unlink(const char *name)
{
    if (name == NULL){
        errno = 1;
        return -1;
    }
    /* 首先将信号量的名称赋值到新建的缓冲区中 */
    char nbuf[20];
    int i;
    for (i = 0; i < 20; i++)
    {
        nbuf[i] = get_fs_byte(name + i);
        if (nbuf[i] == '\0')
            break;
    }
    for (i = 0; i < SEM_LIST_LENGTH; i++)
    {
        if (strcmp(sem_list[i].name, nbuf)==0)
        {
            sem_list[i].active=0;
            return 0;
        }
    }
    return -1;
}

pc.c的话我觉得改一下调用信号量函数的地方就行了。

这部分踩过的坑:

  1. 在用户态和核心态之间传递参数【这个我没考虑到】

    指针参数传递的是应用程序所在地址空间的逻辑地址,
    在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。
    所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据。
    

    这段代码就是在做这个。

    /* 首先将信号量的名称赋值到新建的缓冲区中 */
        char nbuf[20];
        int i = 0;
        for(; i< 20; i++)
        {
        	nbuf[i] = get_fs_byte(name+i);
        }
    
  2. 这一段代码值得学习

    # ifndef _SEM_H_
    # define _SEM_H_
    
  3. 一个第一眼看傻掉了的问题

    //sleep函数的签名
    void sleep_on(struct task_struct **p);
    //一开始初始化队列为空
    sem_list[i].queue = NULL;
    //使用sleep
    sleep_on(&(sem->queue));
    

    如果队列为空的时候,传入sleep_on的是不是NULL呢?

    其实这个本质上是type* p=NULL,&p是不是NULL的问题。虽然知道不是,但还是写个程序测试一下:

    #include <stdio.h> 
    typedef struct {
    	int value;
    }haha;
    
    void isNULL(haha** a){
    	printf("%d",a==NULL);
    }
    
    int main(){
    	haha *h=NULL;
    	isNULL(&h);
    	return 0;
    }
    //result:0
    
  4. sem_post签名与实现矛盾

    wake_up() 的功能是唤醒链表上睡眠的所有进程。
    sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。
    

    以上都是指导书的内容。这个“所有”和“一个”的用意我不大明白。也许唤醒所有进程,其中一个抢到了锁,其他的全睡了,这个也被认为是唤醒其中一个吧()

  5. 聪明的越界处理【未考虑到】

    /* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
    if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
    {
    	return -1;
    }
    

    毕竟有效的信号量都是引用的信号量表的信号量。所以地址越界的自然无效。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/cxylz123/article/details/127129119

哈工大操作系统实验---lab5:信号量的实现与应用_东瓜lqd的博客-爱代码爱编程

实验目的: 加深对进程同步与互斥概念的认识掌握信号量的实现原理(两种不同的实现方式)掌握信号量的使用,并应用它解决生产者-消费者问题 实验内容: 在Linux0.11中实现信号量(原本是没有信号量机制的)在Ubun

(浓缩+精华)哈工大-操作系统-mooc-李治军教授-实验5-信号量的实现与应用_a634238158的博客-爱代码爱编程

操作系统实验5:信号量的实现与应用 实验基本内容:用信号量解决P-V问题,并在0.11实现信号量,用P-V程序检验之。 1.在Ubuntu 上编写应用程序pc.c然后复制到linux0.11上去,pc.c 中

MOOC哈工大操作系统实验5:信号量的实现和应用-爱代码爱编程

1.写在前面: 本实验只完成了信号量的应用,即在linux下写了一个利用信号量解决生产者和消费者的程序。 实验提示中写到,在生产者进程和消费者进程中需要文件读写,我们可以通过标准C的库函数来实现,也可以直接通过对应的系统调用来实现。 如果通过系统调用来实现文件读写的话,文件头应该加上那3行定义的系统调用宏从而进入内核中使用对应的系统调用(像实验二系统调

哈工大OS实验六---信号量的实现和应用-爱代码爱编程

实验步骤参考 原文链接:https://blog.csdn.net/realfancy/article/details/90112982 说明:这个实验我基本都是按着这位博主来做的,虽然出了一点小问题,但是这位博主写的很好,我就再对这里面的代码说明一下吧,大家如果要看完整的步骤,可以去看这位博主的 实验要求: 实现三个函数 sem_open():

【哈工大操作系统】四、进程运行轨迹的跟踪与统计-爱代码爱编程

进程运行轨迹的跟踪与统计 概述编写多进程样本添加log文件描述符添加fprintk()函数寻找状态切换点fork.c中添加新建和就绪sched.c中添加就绪、等待和运行exit.c中添加退出修改时间片时间片轮转法——基本原理时间片大小的影响修改sched.h中的时间片参考 概述 概述:进程从创建(Linux下调用fork())到结束的整个过

哈工大操作系统学习笔记十——信号量与死锁-爱代码爱编程

哈工大os学习笔记十(信号量与死锁) 文章目录 哈工大os学习笔记十(信号量与死锁)一、 信号量临界区保护1.为什么要保护信号量2.临界区3.保护信号量的方法3.1 轮换法3.2 标记法3.3 Peterson算法3.4 面包店算法3.5 硬件方法二、 死锁的处理1.死锁的产生2.死锁的成因3.死锁的必要条件4.死锁的处理方法概述5.死锁的处

哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用-爱代码爱编程

实验5-信号量的实现和应用 实验内容请查看操作系统实验指导手册 绪论 在开始编写代码之前,先理一下这个实验需要做什么以及怎么做: 编写用户态的程序pc.c,在这个程序中,需要创建一个生产者进程,多个消费者进程。创建进程会用到之前学过的fork,为了让这几个进程对同一块内存的读和写不发生错误,需要使用信号量进行控制;信号量的实现需要我们在内

信号量的实现和应用-爱代码爱编程

信号量的实现和应用 一、实验环境 ​ 本次实验的操作环境还是一样的实验环境。环境文件如下: 如果不清楚的话请参考往期博客。 二、实验目标与内容 1、目标: 加深对进程同步与互斥概念的认识; 掌握信号量的使用,并应用它解决生产者——消费者问题; 掌握信号量的实现原理。 2、内容: 本次实验的基本内容是: 在Ubuntu下编写程序,用

进程同步与信号量-爱代码爱编程

进程同步与信号量 1 信号量1.1 信号量的基本结构1.2 信号量临界区保护2 Linux0.11中的进程同步案例参考 1 信号量 1.1 信号量的基本结构 关于如何使用信号量来设计生产者 - 消费者模型这里不做介绍。本节主要是想通过生产者 - 消费者模型来理解信号量的基本结构。如下: #define BUFFER_SIZE 10

哈工大计算机系统大作业——程序人生_chjmlo的博客-爱代码爱编程

  计算机系统 大作业 题     目  程序人生-Hello’s P2P  专       业     航天学院人工智能     学     号     7203610228           班     级     2036015              学       生     韩雨萌    

2022哈工大计算机系统大作业——程序人生_qq_50273056的博客-爱代码爱编程

计算机系统 大作业 题     目  程序人生-Hello’s P2P 专       业  人工智能(未来技术) 学     号   120L020301 班   级   2036011 学       生   张思远 指 导 教 师   刘宏伟 计算机科学与技术学院 2022年5月 摘  

linux 中的 20 大网络监控工具_linux 网络监控-爱代码爱编程

在本教程中,让我们讨论可用于 linux 系统的最佳网络监控工具。有很多可用的工具,如 nethogs、ntopng、nload、iftop、iptraf、bmon、slurm、tcptrack、cbm、netwatch、collectl、trafshow、cacti、etherape、ipband、jnettop、netspeed 和 speedomet