李治军老师操作系统实验5---信号量的实现和应用-信号量可以是-种资源的数量-进程看信号量实现同步即走走停停-信号临界区保护:共享数据的更新过程不能中断-此时切换去执行其他进程,切后进程看的是错信号量

参考文章链接:https://blog.csdn.net/weixin_43166958/article/details/104163221
一、重要资料资料
满时不生产,空时不消耗:
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据
信号量可以是一种资源的数量。资源越多,数据越少
消费者-------消耗资源中数据-------等数据
生产者-------产生数据存入资源----等资源
1️⃣信号量等于0--------------资源中无数据,无消费者无生产者
2️⃣信号量大于0--------------资源中有数据,消费者与生产者可能在流水作业,也希望是流水作业,拥堵肯定是不好的。拥塞是有生产者睡着等资源存储数据
3️⃣信号量小于0--------------资源中无数据,有消费者在睡着等数据

二、本次实验的基本内容是:
1在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题;
2在 0.11 中实现信号量,用生产者—消费者程序检验之。

1
用信号量解决生产者—消费者问题
也可直接先看实验2

实验要求:pc.c程序需打开一个文件buffer.txt作为共享缓冲区,缓冲区同时最多只能保存 10 个数;创建一个生产者进程和N个消费者进程,其中生产者进程向缓冲区写入连续的整数,0,1,2,……,M,M>=500;消费者进程从缓冲区依次读取数字,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和数字输出到标准输出。

(1)
信号量相关函数-------根据这些函数编写生产者消费者模型代码 pc.c
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);

sem_open() 的功能是创建一个信号量,或打开一个已经存在的信号量。如失败,返回值是 NULL
name 是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。
value 是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。

int sem_wait(sem_t *sem);

sem_wait() 就是信号量的 P (生产者)原子操作(硬件原子操作指令与进出栈,中断指令是一样的)。返回 0 表示成功,返回 -1 表示失败。
如果信号量的值比0大,那么进行减一的操作,函数立即返回,往下执行。如果信号量当前为0值,那么调用就会一直阻塞或者直到信号量变得可以进行减一操作,达到阻塞进程的目的.

int sem_post(sem_t *sem);

sem_post() 就是信号量的 V (消费者)原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。
sem_post函数的作用是给信号量的值加上一个“1”,即解锁操作; 它是一个原子操作。

int sem_unlink(const char *name);

sem_unlink() 的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。

off_t lseek(int filedes, off_t offset, int whence);

lseek() 函数可以改变文件的文件偏移量,所有打开的文件都有一个当前文件偏移量,用于表明文件开始处到文件当前位置的字节数。成功返回新的偏移量,失败返回-1。
lseek()便是用来控制该文件的读写位置.
参数fildes 为已打开的文件描述词,
参数offset 为根据参数whence来移动读写位置的位移数.
参数 whence 为下列其中一种:
SEEK_SET 参数offset 即为新的读写位置.
SEEK_CUR 以目前的读写位置往后增加offset 个位移量.
SEEK_END 将读写位置指向文件尾后再增加offset 个位移量. 当whence 值为SEEK_CUR 或SEEK_END 时, 参数offet 允许负值的出现.
下列是教特别的使用方式:
欲将读写位置移到文件开头时:lseek(int fildes, 0, SEEK_SET);
欲将读写位置移到文件尾时:lseek(int fildes, 0, SEEK_END);
想要取得目前文件位置时:lseek(int fildes, 0, SEEK_CUR);
用lseek创建一个空洞文件
空洞文件就是这个文件有一段是空的;
普通文件中间不能有空,write文件时从前往后移动文件指针,依次写入;
用lseek往后跳过一段,就形成空洞文件;
空洞文件对多线程共同操作文件非常有用。需要创建一个很大文件时,从头开始依次创建时间很长,可以将文件分成多段,多个线程操作每个线程负责其中一段的写入。

ssize_t write(int fd, const void *buf, size_t nbyte);

fd:文件描述符;
buf:指定的缓冲区,即指针,指向一段内存单元;
nbyte:要写入文件指定的字节数;
write函数把buf中nbyte写入文件描述符handle所指的文档,成功时返回写的字节数,错误时返回-1.

另一种是: write(const char* str,int n)

str是字符指针或字符数组,用来存放一个字符串。n是int型数,它用来表示输出显示字符串中字符的个数。
write(“string”,strlen(“string”);表示输出字符串常量

(2)
信号量实现生产者消费者模型代码 pc.c:

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

#define M 530    		/*打出数字总数*/
#define N 5       		/*消费者进程数*/
#define BUFSIZE 10      /*缓冲区大小*/

int main()
{
	sem_t *empty, *full, *mutex;/*3个信号量*/
	int fd; /*共享缓冲区文件描述符*/
    int  i,j,k,child;
    int  data;/*写入的数据*/
    pid_t pid;
    int  buf_out = 0;  /*从缓冲区读取位置*/
    int  buf_in = 0;   /*写入缓冲区位置*/
    
	/*打开信号量,O_CREAT|O_EXCL:如果没有指定的信号量就创建,第三个和第四个参数此时是需要的,如果有指定的信号量就打开,第三个和第四个参数此时是不需要的*/
	empty = sem_open("empty", O_CREAT|O_EXCL, 0644, BUFSIZE); /*剩余资源,初始化为size。生产者消耗资源--empty,消费者消耗资源中的数据++empty*/
    full = sem_open("full", O_CREAT|O_EXCL, 0644, 0);         /*已使用资源,初始化为0.生产者消耗资源++full,消费者消耗资源中的数据--full*/
	mutex = sem_open("mutex", O_CREAT|O_EXCL, 0644, 1);       /*互斥量,初始化为1*/
    
    fd = open("buffer.txt", O_CREAT|O_TRUNC|O_RDWR,0666); //只读模式打开或创建文件buffer.txt。 (O_CREAT如果指定文件不存在,则创建这个文件,O_TRUNC如果文件存在,并且以只写/读写方式打开,则清空文件全部内容,O_RDWR读写模式)
    lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);/*lseek v.前后移动。刷新了10*4个字节的缓冲区,可存放10个数字*/
    write(fd,(char *)&buf_out,sizeof(int));//把buf_out中的4字节数据写入文件fd,即将待读取位置存入buffer.txt后,以便子进程之间通信*/
   
   
   
 /*生产者进程*/
    if((pid=fork())==0)
    {
		printf("I'm producer. pid = %d\n", getpid());
		/*生产多少个产品就循环几次*/
        for( i = 0 ; i < M; i++)
        {
			                   /*empty大于0,才能生产*/
            sem_wait(empty);//此时信号量empty为初值10,减一变为9,9大于0不阻塞
            sem_wait(mutex);//此时信号量mutex初值1,减一变为0
                               /*写入一个字符*/
            lseek(fd, buf_in*sizeof(int), SEEK_SET); //新的读写位置变为buf_in*4
            write(fd,(char *)&i,sizeof(int));//将i处4字节的数据写入文件buffer.txt
                                /*更新写入缓冲区位置,保证在0-9之间*/
			                    /*生产完一轮产品(文件缓冲区只能容纳BUFSIZE个产品编号)后*/
			                    /*将缓冲文件的位置指针重新定位到文件首部。*/			
            buf_in = (buf_in + 1) % BUFSIZE;
            sem_post(mutex);
            sem_post(full);     /*共享区中已使用资源++,唤醒消费者线程*/
        }
        printf("producer end.\n");
        fflush(stdout);         /*确保将输出立刻输出到标准输出。*/
        return 0;
    }
	else if(pid < 0)//fork()返回负值:创建子进程失败
    {
        perror("Fail to fork!\n");
        return -1;
    }



/*消费者进程*/
    for( j = 0; j < N ; j++ )//循环fork()N次
    {
        if((pid=fork())==0)//fork()子进程返回0,父进程返回子进程ID
        {
			for( k = 0; k < M/N; k++ )
            {
                sem_wait(full);/*共享区中已使用资源--,一开始为0会阻塞此处*/
                sem_wait(mutex);
				
                /*获得读取位置*/
                lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);
                read(fd,(char *)&buf_out,sizeof(int));
                /*读取数据*/
                lseek(fd,buf_out*sizeof(int),SEEK_SET);
                read(fd,(char *)&data,sizeof(int));
                /*写入读取位置*/
                buf_out = (buf_out + 1) % BUFSIZE;
                lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);
                write(fd,(char *)&buf_out,sizeof(int));

                sem_post(mutex);
                sem_post(empty);/*共享区中剩余资源++,唤醒生产者进程*/
                /*消费资源*/
                printf("%d:  %d\n",getpid(),data);
                fflush(stdout);
            }
			printf("child-%d: pid = %d end.\n", j, getpid());
            return 0;
        }
		else if(pid<0)//fork()返回负值:创建子进程失败
		{
			perror("Fail to fork!\n");
			return -1;
		}
	}



	/*回收线程资源*/
    child = N + 1;
    while(child--)
        wait(NULL);//父进程等待回收所有子进程
    /*释放信号量*/
    sem_unlink("full");
    sem_unlink("empty");
    sem_unlink("mutex");
    /*释放资源*/
    close(fd);
    return 0;
}

(3)
在ubuntu下执行,结果如下:

gcc pc.c -o pc -lpthread
./pc > 1.txt
cat 1.txt  | more

在这里插入图片描述
在这里插入图片描述

2
在 linux-0.11 中实现信号量
在linux-0.11/include/linux目录下新建sem.h,定义信号量的数据结构,包括保存名字和值两个属性以及一个等待进程的队列。

#ifndef _SEM_H
#define _SEM_H
#include <linux/sched.h>
#define SEMTABLE_LEN    20
#define SEM_NAME_LEN    20
typedef struct semaphore
{
    char name[SEM_NAME_LEN];
    int value;
    struct task_struct *queue;
} sem_t;
extern sem_t semtable[SEMTABLE_LEN];
#endif

在linux-0.11/kernel/sem.c目录下新建sem.c,实现信号量

static inline unsigned char get_fs_byte(const char * addr)
{
	unsigned register char _v;

	__asm__ ("movb %%fs:%1,%0"   //movb _v,fs:addr,将内存fs段起始处开始偏移addr的一字节数据存入地址_v
	:"=r" (_v)
	:"m" (*addr));
	return _v;
}

sem_open和sem_unlink函数的实现:
由于sem_open()的第一个参数name,传入的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以要用get_fs_byte()函数获取用户空间的数据。get_fs_byte()函数的功能是获得一个字节的用户空间中的数据。同样,sem_unlink()函数的参数name也要进行相同的处理。
sem_open()函数的功能是新建一个信号量或打开一个已存在的信号量,首先需要将参数字符串从用户空间复制到内核空间,然后对比已经存在的信号量,如果名字相同则直接返回该信号量,否则创建新的信号量。sem_unlink()函数的功能是删除名为 name 的信号量,为了保证系统的多个信号量能在有限长度的数组内都正常工作,当删去信号量时数组后面的元素往前移动,腾出空间。

sem_wait()和sem_post()函数的实现:
可参照实验楼6.4提供的lock_buffer()以及unlock_buffer实现方法。
sem_wait()函数是 P(生产者) 原子操作,功能是使信号量的值减一,如果信号量值为 0 就阻塞当前进程,在 sem_wait()中使用 while 循环,当信号量的值小于等于 0 时保证其他进程一直阻塞;sem_post()函数是 V (消费者)原子操作,功能是使信号量的值加一,如果有等待该信号量的进程,则唤醒其中一个。阻塞和唤醒进程由 kernel/sched.c中的 sleep_on()、wake_up()函数实现,它们的参数都是一个结构体指针—— struct task_struct *,即进程都睡眠或唤醒在该参数指向的一个进程 PCB 结构链表上。
sleep_on()函数参数获取的指针 *p 是等待队列的头指针,每次执行该函数时,指针 *tmp指向原本的等待队列,*p 则指向当前进程,即将当前进程插入等待队列头部,然后将当前进程设为睡眠状态,执行 schedule()进行调度。wake_up()函数将唤醒 *p指向的进程。某个进程被唤醒后,回到 sleep_on()继续执行,它将 *tmp指向的进程继续唤醒,即唤醒等待队列的上一个进程。依次执行下去,等待队列的所有进程都会被唤醒。
完整sem.c代码如下:

#include <linux/sem.h>
#include <linux/sched.h>
#include <unistd.h>
#include <asm/segment.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
//#include <string.h>

sem_t semtable[SEMTABLE_LEN];     /*sem_t信号量的数据结构,所有semtable是信号量数组*/
int cnt = 0;                      /*count计数*/
sem_t *sys_sem_open(const char *name,unsigned int value)
{
    char kernelname[100];         /*在核心态定义一个数组*/
    int isExist = 0;              /*exist n.存在*/       
    int i=0;
    int name_cnt=0;
    while( get_fs_byte(name+name_cnt) != '\0')/*数一数信号量名字有几个字符*/
    name_cnt++;
    if(name_cnt>SEM_NAME_LEN)    /*sem.h中#define SEM_NAME_LEN    20*/
    return NULL;
    for(i=0;i<name_cnt;i++;
    kernelname[i]=get_fs_byte(name+i); /* 从用户态复制到内核态 */
    int name_len = strlen(kernelname);/*C语言strlen函数用来求字符串包含多少个字符*/
    int sem_name_len =0;
    sem_t *p=NULL;
    for(i=0;i<cnt;i++)         /*在信号量数组中寻找是否有信号量const char *name(参数)*/
    {
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
                if( !strcmp(kernelname,semtable[i].name) )/*用于比较两个字符串并根据比较结果返回整数。基本形式为strcmp(str1,str2),若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数*/
                {
                    isExist = 1;
                    break;
                }
        }
    }
    if(isExist == 1)
    {
        p=(sem_t*)(&semtable[i]);
        //printk("find previous name!\n");
    }
    else                     /*在信号量数组中没有找到,新创建一个信号量*/
    {
        i=0;
        for(i=0;i<name_len;i++)
        {
            semtable[cnt].name[i]=kernelname[i];
        }
        semtable[cnt].value = value;
        p=(sem_t*)(&semtable[cnt]);
         //printk("creat name!\n");
        cnt++;
     }
    return p;
}

//STI(Set Interrupt) 中断标志置1指令 使 IF = 1
//CLI(Clear Interrupt) 中断标志置0指令 使 IF = 0
//即:STI 打开中断,CLI关闭中断
//在一个进程中调用sys_sem_wait(),当信号量的值小于等于0时while空转,且该进程无法中断。
//schedule不需要中断,while空转并不会影响在其他进程上下文中影响中断。因为每个进程都在自己的tss
// TSS段中保存了标志寄存器EFLAGS的值,所以在进程切换时CPU中当前EFLAGS的值也随之
// 改变。
int sys_sem_wait(sem_t *sem)        /*P(生产者) 原子操作*/
{
    cli();                          //STI 打开中断,CLI关闭中断
    while( sem->value <= 0 )        //如果信号量值为 0 就阻塞当前进程。因为while(1)在这里一直空转,空转时schedule只能切换到state=0的进程,state=0的进程若是生产者开始生产前也会调用sys_sem_wait()也进入空转。空转实际上是为了等待
        sleep_on(&(sem->queue));    
    sem->value--;                   //value大于零,不阻塞,减value
    sti();
    return 0;
}


int sys_sem_post(sem_t *sem)       /*V (消费者)原子操作*/
{
    cli();
    sem->value++;
    if( (sem->value) <= 1)    //小于0阻塞,只有为1时才唤醒,保证一次仅唤醒一个进程。if不是while这样的循环语句,只执行一次wake_up
        wake_up(&(sem->queue));
    sti();
    return 0;
}


int sys_sem_unlink(const char *name)  //删除名为 name 的信号量
{
    char kernelname[100];   /* 应该足够大了 */
    int isExist = 0;
    int i=0;
    int name_cnt=0;
    while( get_fs_byte(name+name_cnt) != '\0')
            name_cnt++;
    if(name_cnt>SEM_NAME_LEN)
            return NULL;
    for(i=0;i<name_cnt;i++)
            kernelname[i]=get_fs_byte(name+i);
    int name_len = strlen(name);
    int sem_name_len =0;
    for(i=0;i<cnt;i++)
    {
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
                if( !strcmp(kernelname,semtable[i].name))
                {
                        isExist = 1;
                        break;
                }
        }
    }
    if(isExist == 1)
    {
        int tmp=0;
        for(tmp=i;tmp<=cnt;tmp++)
        {
            semtable[tmp]=semtable[tmp+1];
        }
        cnt = cnt-1;
        return 0;
    }
    else
        return -1;
}

修改/include/unistd.h,添加新增的系统调用的编号:

#define __NR_setregid	71
/* 添加系统调用号 */
#define __NR_whoami 72 /* 实验2新增 */
#define __NR_iam    73
#define __NR_sem_open   74  /* 实验5新增 */
#define __NR_sem_wait   75
#define __NR_sem_post   76
#define __NR_sem_unlink 77

修改/kernel/system_call.s,需要修改总的系统调用的和值:

nr_system_calls = 78

修改/include/linux/sys.h,声明新增函数

extern int sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();
fn_ptr sys_call_table[] = {
//...sys_setreuid,sys_setregid,sys_whoami,sys_iam,
sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink
};

修改linux-0.11/kernel目录下的Makefile

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
	panic.o printk.o vsprintf.o sys.o exit.o \
	signal.o mktime.o who.o sem.o
// ...
### Dependencies:
sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h

重新编译内核:make all
修改pc.c,适用于linux-0.11运行:
注意:
(1). 在linux0.11系统的应用程序中,注释不能写//,必须要写/* */
(2). 不能在程序中间对变量定义,比如使用循环时的i要在开始定义,所有变量都必须要在一开始统一定义。

#define   __LIBRARY__
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.h>

_syscall2(sem_t*,sem_open,const char *,name,unsigned int,value);
_syscall1(int,sem_wait,sem_t*,sem);
_syscall1(int,sem_post,sem_t*,sem);
_syscall1(int,sem_unlink,const char *,name);

int main()
{
    ...
    /*打开信号量*/
	 if((mutex = sem_open("mutex",1)) == NULL)
    {
        perror("sem_open() error!\n");
        return -1;
    }
    if((empty = sem_open("empty",10)) == NULL)
    {
        perror("sem_open() error!\n");
        return -1;
    }
    if((full = sem_open("full",0)) == NULL)
    {
        perror("sem_open() error!\n");
        return -1;
    }
    ...
}

将已经修改的/usr/include/unistd.h和sem.h文件以及新修改的pc.c拷贝到linux-0.11系统中,用于测试实现的信号量。

sudo ./mount-hdc 
cp ./unistd.h ./hdc/usr/include/
cp ./sem.h ./hdc/usr/include/linux/
cp ./pc.c ./hdc/usr/root/
sudo umount hdc/

启动新编译的linux-0.11内核,用pc.c测试实现的信号量

./run
gcc -o pc pc.c
./pc > 1.txt
sync

关闭linux-0.11,挂载虚拟磁盘,查看信息输出文件

sudo ./mount-hdc
sudo cp ./hdc/usr/root/1.txt ./
sudo chmod 777 1.txt
cat 1.txt | more

在这里插入图片描述
。。。。。。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
操作系统 治军 pdf》是一本由作者治军撰写的关于操作系统的书籍的电子版PDF文件。操作系统是计算机系统中的一个核心组成部分,它负责管理和控制计算机的硬件和软件资源,使得计算机能够高效地运行各种应用程序。 在这本书中,治军详细介绍了操作系统的基本概念、原理和设计原则。他从操作系统的起源和发展历程出发,讲解了多道、分时和实时操作系统等不同类型的操作系统,并深入解析了其内核结构和功能。此外,书中还讨论了进程管理、内存管理、文件系统、输入输出控制等重要的操作系统主题,为读者提供了全面了解和深入学习的机会。 这本书的PDF版本使得读者可以更加便捷地获取其中的内容,无论是在电脑、平板还是手机上都可以进行阅读。通过阅读这本书,读者可以深入了解操作系统的基本原理和相关技术,有助于他们提升对计算机系统的理解和应用。对于学习计算机科学或相关专业的学生和从事软件开发工作的技术人员来说,这本书是一本宝贵的参考资料。 总之,《操作系统 治军 pdf》是一本全面介绍操作系统的书籍的电子版PDF文件,读者可以通过阅读它来深入了解操作系统的原理和技术。这本书的出现为学习和研究操作系统提供了便利,对于对计算机系统有兴趣的人士来说是一本值得阅读的优秀教材。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值