对比标准POSIX信号量,只考虑有名信号量:使用 Posix IPC 名字标识,可用于进程或线程间的同步。
一个进程可以在信号量上执行三种操作:
创建。要求调用者指定初始值,对于二值信号量来说, 它通常是1,但也可以是0。
等待。该操作会测试会这个信号量的值,若其值 <= 0,那就等待(阻塞),一旦其值变为 >0 就将它减 1。测试和减 1 必须要是原子操作。
挂出。挂出一个信号量,会将信号量的值加 1,若某个进程阻塞等待该信号量的值变为大于 0,其现在就可能被唤醒。同时挂出操作也必须是原子的。
(先新建sem.c)中实现如下系统调用函数
sem_t *sys_sem_open(const char *name, unsigned int value);
int sys_sem_wait(sem_t *sem);
int sys_sem_post(sem_t *sem);
int sys_sem_unlink(const char *name);
挂载虚拟机,在(~/oslab/hdc/usr/include/linux)中创建sem.h,定义数据类型sem_t:
#ifndef _SEM_H_
#define _SEM_H_
#include <linux/sched.h> //用到了task_struct
/* 定义因某种信号量而阻塞的队列 */
#define QUE_LEN 16
struct semaphore_queue {
int front;
int rear;
struct task_struct *wait_tasks[QUE_LEN]; //信号量表征资源的使用情况,进程因缺少资源而阻塞,挂在该信号量的等待队列上;
};
typedef struct semaphore_queue sem_queue;
/* 定义sem_t */
struct semaphore_t {
int value; //信号量的值
int occupied; //是否被使用了
char name[32]; //信号量的名字
struct semaphore_queue wait_queue;
};
typedef struct semaphore_t sem_t;
#endif
kernel/sem.c的源码及注释
//#include <unistd.h> //包括了sem_t的定义等
#include <linux/sem.h> //sem_t的定义的等
#include <linux/sched.h> //使用了schedule函数
#include <linux/kernel.h> //使用了printk函数
#include <asm/system.h> //cli(), sti(), 实现原子操作
#include <asm/segment.h> //使用了get_fs_byte函数
#define NAME_SIZE 32
/* 信号量本质就是一种结构体,所以要消耗内存资源,故是有限的 */
//用数组来管理
#define SEM_COUNT 32
sem_t semaphores[SEM_COUNT];
/* 判断某信号量的等待队列是否已满 */
//循环队列:空1个位置,以判断队满
int is_full(sem_queue *q) {
if((q->rear+1) % QUE_LEN == q->front)
return 1; //1表示满了
else return 0;
}
/* 进程被阻塞,挂到某个信号量的等待队列中;*/
int insert_task(struct task_struct *p, sem_queue *q) {
if(is_full(q)) {
printk("Queue is full!\n");
return -1; //插入失败
}
else {
//入队操作
q->wait_tasks[q->rear] = p;
q->rear = (q->rear + 1) % QUE_LEN;
return 1; //插入成功
}
}
/* 判断某信号量的等待队列是否为空 */
int is_empty(sem_queue *q) {
return q->rear == q->front ? 1 : 0; //1表示为空
}
/* 获得循环队列的第1个任务 */
struct task_struct *get_task(sem_queue *q) {
if(is_empty(q)) {
printk("Queue is empty!\n");
return NULL;
}
else {
struct task_struct *tmp = q->wait_tasks[q->front];
q->front = (q->front + 1) % QUE_LEN;
return tmp;
}
}
/* 判断信号量是否已经打开了 */
//信号量的名字是唯一的,所以根据名字来判断
int is_sem_opened(const char *name, int name_size) {
int i;
for(i = 0; i < SEM_COUNT; i++) {
if(semaphores[i].occupied == 1) {
int j;
int flag = 1; //表示名字匹配上了
for(j = 0; j < name_size; j++) {
if(name[j] != semaphores[i].name[j]) {
flag = 0;
break;
}
}
if(flag) return i;
}
}
return -1; //没打开。
}
/* 初始化信号量的等待队列 */
void init_queue(sem_queue *q) {
q->front = q->rear = 0;
}
/*
sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量。
*/
sem_t *sys_sem_open(const char *name, unsigned int value) {
char sem_name[NAME_SIZE];
int i;
for(i = 0; i < NAME_SIZE; i++) {
char c = get_fs_byte(name + i);
sem_name[i] = c;
if(c == '\0') break;
}
if(i == NAME_SIZE) {
printk("The name of semaphore is too long!\n");
return NULL;
}
int id;
if((id = is_sem_opened(sem_name, i)) != -1) {
//打开了
return &semaphores[id];
}
/* 没打开,则打开 */
int j;
for(j = 0; j < SEM_COUNT; j++) {
if(semaphores[j].occupied == 0) {
/* 复制名字 */
int tmp;
for(tmp = 0; tmp <= i; tmp++) {
semaphores[j].name[tmp] = sem_name[tmp];
}
semaphores[j].occupied = 1;
semaphores[j].value = value;
//初始化该信号量的等待队列
init_queue(&(semaphores[j].wait_queue));
return &semaphores[j];
}
}
/* 信号量数组已经满了 */
printk("Numbers of semaphores are limited!\n");
return NULL;
}
/* 释放信号量资源 */
int sys_sem_unlink(const char *name) {
char sem_name[NAME_SIZE];
int i;
for(i = 0; i < NAME_SIZE; i++) {
char c = get_fs_byte(name + i);
sem_name[i] = c;
if(c == '\0') break;
}
if(i == NAME_SIZE) {
printk("The name of semaphore is too long!\n");
return -1;
}
/* 判断是否打开了 */
int id;
if((id = is_sem_opened(sem_name, i)) != -1) {
//打开了,才释放
semaphores[id].occupied = 0;
return 0;
}
return -1;
}
/*
关闭一个信号量并没有将它从系统中删除。
Posix 有名信号量至少是随内核持续:即使当前没有进程打开着某个信号量,它的值仍然保持。
*/
/* P操作 */
int sys_sem_wait(sem_t *sem) {
cli();
sem->value--;
if(sem->value < 0) {
current->state = TASK_UNINTERRUPTIBLE;
insert_task(current, &(sem->wait_queue));
schedule();
}
sti();
return 0;
}
/* V操作 */
int sys_sem_post(sem_t *sem) {
cli();
sem->value++;
if(sem->value <= 0) {
struct task_struct *p = get_task(&(sem->wait_queue));
if(p != NULL) {
p->state = TASK_RUNNING;
}
}
sti();
return 0;
}
添加系统调用
unistd.h
添加宏定义
system_call.s
修改数量
在include/linux/sys.h中添加系统调用的定义:
/* ... */
extern int sys_setregid();
/* 添加的系统调用定义 */
#include<linux/sem.h>
extern sem_t * sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();
/* 在sys_call_table数组中添加系统调用的引用: */
fn_ptr sys_call_table[] =
{ sys_setup, sys_exit, sys_fork, sys_read,......, sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink},
修改 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 sem.o
/* 第二处 */
### Dependencies:
sem.s sem.o: sem.c ../include/linux/kernel.h ../include/unistd.h \
../include/linux/sem.h ../include/linux/sched.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
../include/asm/segment.h
至此,我们就完成了系统内核sem.c
编写,并修改了系统文件让四个函数能正常运行。
使用make all
指令重新编译linux0.11内核,如果有错误出现的话,按照系统提示依次修改,直至编译成功。
接下来,我们继续编写用户态的pc.c
。
用户态pc.c
/*
建立一个生产者进程,N个消费者进程(N>1);
用文件建立一个共享缓冲区;
生产者进程依次向缓冲区写入整数0,1,2,...,M,M>=500;
消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程ID和数字输出到标准输出;
缓冲区同时最多只能保存10个数。
提示:建议直接使用系统调用进行文件操作,使用标准C的fopen等文件读写函数需要额外的操作,稍显麻烦
*/
#define __LIBRARY__
#include <stdio.h>
#include <linux/sem.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.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 consumerNum = 5; /* 消费者个数 */
const int maxNum = 500; /* 写入的数据量 */
const int bufSize = 10; /* 缓冲区大小 */
int data = -404; /* 存放读取的数据*/
pid_t p_pid[5]; /* 进程号数组*/
sem_t *empty, *full, *mutex; /*三个信号量 */
void producer(){
int i, fo;
int endpos_produce = 0; /* 记录消费者进程的消费次数*/
fo = open("report.txt", O_WRONLY | O_TRUNC, 0666);
if (!fo){
perror("打开文件失败!\n");
return;
}
/* 这一步是提前在文件中写入初始的endpos_consumer的值 */
lseek(fo, bufSize * sizeof(int), SEEK_SET);
write(fo, &endpos_produce, sizeof(int));
for (i = 0; i <= maxNum; i++){
sem_wait(empty); /* 如果empty为0,意味着缓冲区已满,阻塞生产者*/
sem_wait(mutex); /* 互斥控制 */
lseek(fo, endpos_produce * sizeof(int), SEEK_SET);
write(fo, &i, sizeof(int));
endpos_produce = (endpos_produce + 1) % bufSize;
sem_post(mutex); /* 出了临界区,需要mutex++,以便下一次可以进入 */
sem_post(full); /* 看是不是需要唤醒阻塞 */
}
close(fo);
}
void consumer(){
int cnt, endpos_consumer, fi;
fi = open("report.txt", O_RDONLY, 0666);
if (!fi){
perror("创建文件缓冲区失败!\n");
return;
}
for (cnt = 0; cnt < maxNum / 5; ++cnt){
sem_wait(full);
sem_wait(mutex);
/* 从文件中读取消费者进程的文件指针位置 */
lseek(fi, bufSize * sizeof(int), SEEK_SET);
read(fi, &endpos_consumer, sizeof(int));
/* 将文件中对应位置的数据读取出来 */
lseek(fi, endpos_consumer * sizeof(int), SEEK_SET);
read(fi, &data, sizeof(int));
printf("\nC,%d: %d...\n", getpid(), data);
fflush(stdout);
/* 移动endpos_consumer的位置,下一次的消费者进程将读取新的数*/
endpos_consumer = (endpos_consumer + 1) % bufSize;
lseek(fi, bufSize * sizeof(int), SEEK_SET);
write(fi, &endpos_consumer, sizeof(int));
sem_post(mutex);
sem_post(empty);
}
close(fi);
}
int main(){
/* 建立这三个信号量 */
empty = sem_open("empty", bufSize);
if (empty == NULL){
perror("empty create falied!\n");
return -1;
}
full = sem_open("full", 0);
if (full == NULL){
perror("full create failed!\n");
return -1;
}
mutex = sem_open("mutex", 1);
if (mutex == NULL){
perror("mutex create failed!\n");
return -1;
}
if (empty && full && mutex){
printf("create semphore successed!\n");
}
/* 调用生产者进程,往文件缓冲区中写入数字 */
if (!fork()){
printf("producer is running!\n");
producer();
exit(0); /* 生产者任务完成后,杀死该子进程 */
}
while (consumerNum--){
if (!fork()){
printf("consumer %d is running!\n", getpid());
consumer();
exit(0);
}
}
wait(NULL);
/* 关闭信号量 */
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
return 0;
}