一.信号量与PV操作概述
在多道程序系统中,由于资源共享与进程合作,使各进程之间可能产生两种形式的制约关系,一种是间接相互制约,例如,在仅有一台打印机的系统,同一时刻只能有一个进程分配到到打印机,其他进程必须阻塞;另一种是直接相互制约,例如进程A通过单缓冲去向进程B提供数据,当改缓冲区为空时,进程B不能获取所需的数据而阻塞,一旦进程A将数据送入缓冲区,进程B就被唤醒,反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲区的数据时,才唤醒进程A。
进程同步主要源于进程合作,是进程之间共同完成一项任务时直接发生相互作用的关系,为进程之间的直接制约关系。
进程互斥主要源于资源共享是进程之间的间接制约关系。在多道程序系统中,每次只允许一个进程访问的资源称为临界资源,进程互斥要求保证每次只有一个进程使用临界资源。在每个进程中访问临界资源的程序段称为临界区,进程进入临界区要满足一定的条件,以保证临界资源的安全使用和系统的正常运行。
PV操作是对信号量进行处理的操作过程,而且信号量只能由PV操作来改变。P操作是对信号量减去1,意味着请求系统分配一个单位资源,若系统无可用资源,则进程变为阻塞状态;V操作是对信号量加1,意味着释放一个资源,加1后若信号量小于等于0,则从就绪队列中唤醒一个进程,执行V操作的进程继续执行。
二.Linux系统下程序示例
在Linux系统中,通常调用semget,semctl,semop这些API来实现。
首先,需要调用 semget函数创建信号量或获得在系统中已存在的信号量。不同进程通过使用同一个信号量键值来获得同一个信号量。其次,使用 semctl函数的SETVAL操作来初始化信号量,此时。当使用二维信号量时,通常将信号量初始化为1。接下来就是调用 semop函数进行信号量的PV操作。最后如果不需要信号量,则使用semctl函数的 IPC_RMID操作从系统中删除它。
在这里以Linux下的C程序与例子,简单验证下使用信号量实现PV操作的原理。
1.在Linux下创建一个名为pv_test的工程目录。
2.创建一个sem_pv.c的文件,写入以下代码。
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdarg.h>
#include <time.h>
#define SEM_P -1
#define SEM_V 1
#if !(defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED))
//如果系统中没有定义这个联合体,则需要额外定义一下
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
#endif
//获取时间戳
int get_timestamp(char *timestamp)
{
time_t timep;
struct tm *p, pp;
time(&timep);
localtime_r(&timep, &pp);
p = &pp;
return sprintf(timestamp, "%04d-%02d-%02d %02d:%02d:%02d", p->tm_year + 1900, p->tm_mon + 1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec);
}
/*
日志打印
*/
void LOG(const char *format,...)
{
va_list argptr;
char buffer[2048];
va_start(argptr,format);
vsprintf(buffer,format,argptr);
va_end(argptr);
char timestamp[64]="";
get_timestamp(timestamp);
printf("[%s][%d]%s", timestamp, getpid(), buffer);
}
int sem_init(int semid)
{
int ret;
union semun semun;
semun.val = 0;
ret = semctl(semid, 0, SETVAL, semun);
if (ret == -1) {
LOG("semctl failed!\n");
}
return ret;
}
int semop_pv(int semid, int opt)
{
int ret;
struct sembuf sembuf;
sembuf.sem_op = opt;
sembuf.sem_num = 0;//初始化为0,表示暂时没资源
sembuf.sem_flg = SEM_UNDO;
ret = semop(semid, &sembuf, 1);
if (ret == -1) {
LOG("sem_p failed!\n");
}
return ret;
}
/*
模拟P操作
*/
int semop_p(int semid)
{
//p操作 对信号量进行减1
LOG("semctl P\n");
return semop_pv(semid, SEM_P);
}
/*
模拟V操作
*/
int semop_v(int semid)
{
//v操作 对信号量进行加1
LOG("semctl V\n");
return semop_pv(semid, SEM_V);
}
/*
删除信号量
*/
void sem_del(int semid)
{
union semun sem_un;
if(semctl(semid, 0, IPC_RMID, sem_un)<0) {
LOG("sem_del fail\n");
}
}
3.创建一个名为“main.c”的文件,写入以下代码:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <time.h>
#define TEST_FILE "./test"
int file_write()
{
char timestamp[64]="";
FILE *fp = fopen(TEST_FILE, "ab+");
if(!fp) {
LOG("[%s](%d)open %s failed!\n", __func__, __LINE__, TEST_FILE);
return -1;
}
get_timestamp(timestamp);
//将进程号写入文件
fprintf(fp, "[%s]Process (%d)write!!!\n", timestamp, getpid());
fclose(fp);
return 0;
}
int main(int argc, char* argv[])
{
int i;
int ret;
int semid;
int timeout = 5;//测试写文件操作5次
int sleep_second = 1;
/* 获取信号量 键值设置为6666,
1表示信号量的数量,
后面表示如果没有这个信号量就创建而且设置为都可以访问的权限*/
semid = semget((key_t)6666, 1, 0666 | IPC_CREAT);
/*如果没有创建成功就会返回-1 然后打印失败的信息*/
LOG("semid=%d\n", semid);
if (semid == -1) {
LOG("semget failed!\n");
exit(1);
}
/* 初始化信号量 */
ret = sem_init(semid);
if (ret == -1) {
exit(1);
}
if(argc<2) {
LOG("Input error!!!\n");
exit(0);
}
int param = atoi(argv[1]);
if(param==1) {
//参数为1的进程进行V操作
if (semop_v(semid) == -1) {
exit(1);
}
sleep(1);
}
else {
unlink(TEST_FILE);
}
while(timeout--) {
//刚开始
if (semop_p(semid) == -1) {
exit(1);
}
/* 临界区操作*/
LOG("begin write\n");
file_write();
sleep(sleep_second);
LOG("write done!\n");
if (semop_v(semid) == -1) {
exit(1);
}
}
/* 删除信号量 */
if(param==1) {
sem_del(semid);
}
return 0;
}
4.创建一个Makefile文件,写入以下内容:
CPROG = pv_test
BIN = $(CPROG)
CC= gcc
OBJS=main.o sem_pv.o
all: $(BIN)
clean:
rm -f $(OBJS) $(BIN)
$(BIN): $(OBJS)
$(CC) -o $(BIN) $(OBJS) $(CFLAGS) $(LDFLAGS) $(CFLAGS_EXTRA)
5.在当前目录下执行make clean;make编译生成一个pv_test可执行文件
6.测试。运行时,使用参数0和1来区分不同的进程。
查看写入的文件,可以看到两个进程交替写文件,如下所示:
总结:
信号量不仅是一种进程之间的通信机制,与此同时,信号量的使用,可以有效的解决进程间的同步与互斥问题。信号量的PV操作是实现进程间的同步和互斥的核心工作部分。