Linux Posix 编程初步——信号量小讲(哈工大操作系统实验4)

时间:2022-10-31 15:13:42

软件学院第四次实验的确有点小蛋疼,不过幸好做完了,为了纪念一下,决定写一篇小博文。(ps.每次去实验室,都有一种想死的感觉,不过也幸好去了一下,发现自己有很多概念不明白,现在明白了,给大家解释一下)

先讲一讲信号量的原理吧,昨天晚上给室友讲了一下,觉得这个例子特别好,就是比如说有一个屋子10个房间里面都住着人(进程),但是只有3个厕所(资源),当然,每个人都想用厕所,并且自己用的时候不想让别人看着(这不是废话),于是,Posix就出现了,也就是信号灯,我用厕所的时候,我就先把厕所的灯(信号量的值)打开,如果有灯,则表示厕所被占用了,你一出房间门,发现亮了3盏灯,说明TMD已经没厕所了,又想上厕所,只能去等着,于是,就出现了等待队列,这时候有人用完了出来,关灯出来(V操作),看见有人等着,就把你喊过去,你进厕所,拉灯(P操作),就可以用厕所了。这个就是整个信号量操作了。

 

信号量就是为了解决进程同步的。也就是说有一些系统资源不能够被同时使用,于是需要一个锁,来确保不会被同时使用。

除了互斥锁(Value=1),信号量也用做进程间同步,其实上一个例子就可以看出。

 

接下来看一下我们的实验。

首先是需求分析。

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

  1. 建立一个生产者进程,N个消费者进程(N>1);
  2. 文件建立一个共享缓冲区;
  3. 生产者进程依次向缓冲区写入整数0,1,2,...,M,M>=500;
  4. 消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程ID和数字输出到标准输出;
  5. 缓冲区同时最多只能保存10个数。


这个是实验的第一步,用Ubuntu的现有的信号量来实现进程同步和互斥基本上就是需要熟练的掌握文件操作和理解信号量原理就可以完成。

先说一下文件操作:

 

            FILE* a
a = fopen("in.txt","ab+");
if(a == NULL)
{
printf("file open error!\n");
return -1;
}
fwrite(&num,sizeof(int),1,a);

这样的话,就可以从in.txt文件中读取1个int到num这个int型变量中了。。

然后是读操作,文件指针依旧一样,只要把写语句换成读:

           fread(&num,sizeof(int),1,f1);

这样的话,很轻松就完成了基本的c语言文件操作;

 

接下来考虑信号量得设置:

首先,文件一定是一个临界区,所以需要用一个互斥锁锁住;

但是光光互斥就够了吗?对,想起老师怎么说的了没?对,一个互斥锁,在一些特殊得cpu调度的情况下就会死锁,这里就不举例子了。

于是,我们再创建两个信号量,一个用来表示生产了多少个,一个用来表示还剩多少个空位。

if((semPosix = sem_open("1",O_CREAT,0777,1)) == SEM_FAILED)
printf("Sem_t1 error!");
if((semPossess = sem_open("2",O_CREAT,0777,0)) == SEM_FAILED)
printf("Sem_t2 error!");
if((semEmpty = sem_open("3",O_CREAT,0777,10)) == SEM_FAILED)
printf("Sem_t3 error!");

sem_open ("信号量名称",O_CREAT,0777,value)这里O_CREAT表示创建一个进程间的信号量,0777表示给最大权限。(详细参看Linux权限定义)

这样,我们信号量就创建好了。这里有一个小问题,open函数能够在内核创建一个信号量,也能够打开一个已有的信号量(根据信号量名称来判断),在调试过程中,如果你想用一个全新的信号量,记得关掉之前的,或者换名字。

 

接下来就是信号量逻辑:这个老师上课讲了很多我这立就直接给出,等我想到了怎么描述再来补全:

 

生产者:

P(Empty)

P(Posix)

生产

V(Posix)

V(Prossess)

消费者:

P(Prossess)

P(Posix)

消费

V(Posix)

V(Empty)

这样就不会出现死锁。具体得还需要读者好好琢磨。这里给出生产者代码供大家参详:

            sem_wait(semEmpty);
sem_wait(semPosix);
fin = fopen("in.txt","ab+");
if(fin == NULL)
{
printf("file open error!\n");
return -1;
}
fwrite(&num,sizeof(int),1,fin);
num++;
fclose(fin);
sem_post(semPosix);
sem_post(semPossess);

大家可以对比一下上面的逻辑来看看。。。

 而消费者的话,则需要通过一个额外的功能,就是从缓冲区消费掉一个,怎么做呢?不少同学采用文件指针的方法,不停的寻找,覆盖,这里我就采用一个新方法,更加简单直接:

因为需求上只有10个大小的缓冲区,于是准备一个10个大小的数组,一次性读取后,重新以截断形式打开,只写入9个,轻松模拟了消费一个的操作。

具体提供代码:

            f1 = fopen("in.txt","rb+");
if(f1 == NULL)
{
printf("file open error!\n");
return;
}
for(i = 0;i<10;i++)
{
num = -1;
fread(&num,sizeof(int),1,f1);
arr[i] = num;
}
fclose(f1);

这样的话,已经读到了10个到arr数组,如果不到10个,后几个都为初始值-1;接下来:

            printf("I am child%d :%d\n",getpid(),arr[0]);
f1 = fopen("in.txt","wb");
for(i = 1;i<10;i++)
{
if(arr[i]>0)
fwrite(&(arr[i]),sizeof(int),1,f1);
}
fclose(f1);

以“wb”打开后,截断之前所有内容,写回9个;

 

我看了一下我的pc.c发现只有fork部分代码没有贴出来了,大家参照第三次实验,很简单把。

于是,我们已经把Ubuntu下pc.c写完了,也就意味着,我们已经解决了50%的得分。

 

 

接下来,就是在Linux0.11实现信号量。这里就采用到我上一篇博客的内容,有遗忘的读者可以去看看,这里就不多说了,

4个系统调用

_syscall2(int,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);

我们一个一个来完成;

 

首先是实现数据结构:

typedef struct __queue{
unsigned int ID;
unsigned int lock;
struct task_struct* task;
struct __queue* next;
} queue;

typedef struct __sem_t{
unsigned int value;
char* name;
queue* head;
queue* now;
unsigned int ID;
} sem_t;

struct task_struct* __task = NULL;
sem_t* __gloable[64];
char __semname[64][24];
int __semnum = 0;

这里实现了两个结构体和4个全局变量,在队列的选择上比较倾向于链表实现,而且的确有优点,读者如果发现段错误的话,基本上就是链表指空了或者数组越界了等等这种内存引用的错误。

解释一下含义:

首先是__task:这是一个隐式链表(指导书上是这么说的),我们所有的睡眠队列就是这里,因为里面不知道有什么,于是我在外面又包装了一层,就是下面得queue;

queue:这个就是我定义得等待队列了,这样的话,我就可以实现每一个task有一个lock(锁值),还有里面有一个ID,就是承载task的pid,方便调试的时候打印所有队列来看进程等待状况;

sem_t:这个就是信号量的单元,里面有一个队列(head为队列头),和now指针(指向当前节点)。在pc.c中我们用到3个信号量,每一个都必须有个一等待队列;而真正睡眠的队列只有一条,也就是__task,在queue里保存的是那个任务的指针和lock值,由于不会操作__task链表,我们只能想办法自己建一个链表模拟操作,不知到这么说是不是明白一点。稍后贴个图来说明;

 

__gloable:全局变量承载最多64个信号量值;

__senname:信号量名字;

__semnum:当前信号量个数;

 

好了,说了这么多,多少说清楚了一点吧,接下来看代码实现:

 

open函数:

首先是内核态和用户态的传值(上一次实验有做,由于没写博客,这里就解释一下):

    for(num=0;num<23;num++)
{
b[num] ='\0';
}
num = 0;
while((temp = get_fs_byte(name+num)) != '\0')
{
if(num<23)
{
b[num] = temp;
num++;
}
else
{
return NULL;
}
}
b[num] = '\0';

通过get_fs_byte()函数循环从用户态读字符到内核态,具体直接参详代码;

之后,就是查找重名的信号量,如果有的话直接返回就好了:

    cli();
for(n = 0;n<__semnum;n++)
{
if(strcmp(__gloable[n]->name,b)==0)
{
sti();
return __gloable[n];
}
}
sti();

这里涉及对全局变量的操作,采用关中断保护一下(其实应该没问题的);另外,说明一点,strcmp函数不要包含头文件<string.h>否则就会造成函数改变相互比较的字符串,原因是如果有头文件的话就自动內连执行在0.11这种版本里出现问题。

没找到的话就新建一个信号量(初始化过程):

    cli();
__gloable[__semnum] = malloc(sizeof(sem_t));
for(n = 0;n<24;n++)
{
__semname[__semnum][n] = b[n];
}
__gloable[__semnum]->name = __semname[__semnum];
__gloable[__semnum]->value = value;
__gloable[__semnum]->ID = __semnum;
__gloable[__semnum]->head = NULL;
__gloable[__semnum]->now = NULL;
__semnum++;
sti();

 

这样,open函数就好了;

然后是unlink函数(用户态和内核态传递信息是一样的,只有后面的不一样):

    cli();
for(n = 0;n<__semnum;n++)
{
if(strcmp(__gloable[n]->name,b)==0)
{
free(__gloable[n]);
__gloable[n] = NULL;
sti();
return 0;
}
}
sti();

 

 

接下来就是wait函数,也就是信号量P操作:

    int value = 0;
queue* temp;
cli();
(sem->value) = (sem->value)-1;
value = sem->value;
sti();
if(value< 0)
{
cli();
if(sem->head == NULL)
{
sem->head = malloc(sizeof(queue));
sem->head->ID = current->pid;
sem->head->task = current;
sem->head->next = NULL;
sem->head->lock = 1;
sem->now = sem->head;
}
else
{
sem->now->next = malloc(sizeof(queue));
sem->now = sem->now->next;
sem->now->ID = current->pid;
sem->now->task = current;
sem->now->lock = 1;
sem->now->next = NULL;
}
temp = sem->now;
sti();
while(temp ->lock == 1)
sleep_on(__task);
free(temp);
}

 

这里就需要讲解一下,首先需要的原子操作,中间不能被调度,也就是cli()关中断,和sti()开中断。
有没有觉得这句是多余的,明明可以用sem->value直接比较?

    value = sem->value;

当然不是记不记得sem->value是无符号数,所以就算value等于-1,依然大于0;所以赋值给int型就可以判断了;

当value值小于0时,也就是资源数不够用了,那么就要加入等待队列;

进入队列就是添加操作,给lock赋值1(表示锁住了),给task赋值current,这个是全局变量,记录着当前进程。

接下来就是一个while循环,如果lock值为1,就睡眠在__task队列,如果被唤醒,则再次检查lock值。当然,有些不懂看下一个post函数;

 

最后就是post函数,也就是实现信号量的V操作:

    cli();
int temp = sem->value;
sem->value++;
if(temp <0)
{
sem->head->lock = 0;
wake_up(__task);
sem->head = sem->head->next;
}
sti();

首先需要的原子操作,中间不能被调度,也就是cli()关中断,和sti()开中断。

sem->head->lock = 0;

这句就是把这个信号量的队列的锁值制成0(开锁),由于wait的时候lock初始值一直是1,这里把一个制成0后,唤醒整个队列(__task),上面得while循环判断就可以被用到,判断lock值。发现只有一个进程的lock被置为0,那个进程就跳出while循环,不用接着睡眠了。

这样,我们的post,和wait就写完了。

 

至此,基本上就完工了。

 

 介于不少同学没有头文件,这里给出:

 

linux0.11中pc.c

#define __LIBRARY__
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <time.h>
#include <sys/times.h>
#include <signal.h>
#include <sys/stat.h>

 


sem.c

#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <utime.h>
#include <sys/stat.h>
#include <linux/sched.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <linux/fdreg.h>
#include <asm/io.h>
#include <signal.h>