首先了解一下,信号量机概念是由荷兰科学家Dijkstr引入,值得一提的是,它提出的Dijksrtr算法解决了最短路径问题。

信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况,信号量是一个特殊的变量,并且只有两个操作可以改变其值:等待(wait)与信号(signal)。

因为在Linux与UNIX编程中,"wait"与"signal"已经具有特殊的意义了(暂不知这特殊意义是啥),所以原始概念:
     用于等待(wait)的P(信号量变量) ;
     用于信号(signal)的V(信号量变量) ;
这两字母来自等待(passeren:通过,如同临界区前的检测点)与信号(vrjgeven:指定或释放,如同释放临界区的控制权)的荷兰语。

P操作 负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。

操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞;

V操作 负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。

操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。

补充:查看共享信息的内存的命令是ipcs [-m|-s|-q] (全部的话是ipcs -a) ;查看共享信息的内存的命令是ipcs [-m|-s|-q]。

(一)系统调用函数semget()

函数原型:int semget(key_t key,int nsems,int semflg);

功能描述: 创建一个新的信号量集,或者存取一个已经存在的信号量集。

当调用semget创建一个信号量时,他的相应的semid_ds结构被初始化。ipc_perm中各个量被设置为相应
值:
        sem_nsems被设置为nsems所示的值;    
        sem_otime被设置为0; 
        sem_ctime被设置为当前时间

参数介绍:
         key:所创建或打开信号量集的键值,键值是IPC_PRIVATE,该值通常为0,创建一个仅能被进程进程给我的信号量, 键值不是IPC_PRIVATE,我们可以指定键值,例如1234;也可以一个ftok()函数来取得一个唯一的键值。
         nsems:创建的信号量集中的信号量的个数,该参数只在创建信号量集时有效。
         semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过or表示:

有IPC_CREAT,IPC_EXCL两种:

IPC_CREAT如果信号量不存在,则创建一个信号量,否则获取。

IPC_EXCL只有信号量不存在的时候,新的信号量才建立,否则就产生错误。

返回值说明:
如果成功,则返回信号量集的IPC标识符,其作用与信息队列识符一样。
如果失败,则返回-1,errno被设定成以下的某个值
EACCES:没有访问该信号量集的权限
EEXIST:信号量集已经存在,无法创建
EINVAL:参数nsems的值小于0或者大于该信号量集的限制;或者是该key关联的信号量集已存在,并且nsems
大于该信号量集的信号量数
ENOENT:信号量集不存在,同时没有使用IPC_CREAT
ENOMEM :没有足够的内存创建新的信号量集
ENOSPC:超出系统限制

图解:

每个信号量都有一些相关值:

semval 信号量的值,一般是一个正整数,它只能通过信号量系统调用semctl函数设置,程序无法直接对它进行修改。

sempid 最后一个对信号量进行操作的进程的pid.

semcnt 等待信号量的值大于其当前值的进程数。

semzcnt 等待信号量的值归零的进程数。

 

(二)信号量的控制 semctl()

原型:int semctl(int semid,int semnum,int cmd,union semun ctl_arg); 
参数介绍: semid为信号量集引用标志符,即semget 的返回值。 
               semnum第二个参数是信号量数目;

cmd表示调用该函数执行的操作,其取值和对应操作如下:

标准的IPC函数

(注意在头文件<sys/sem.h>中包含semid_ds结构的定义)

IPC_STAT 把状态信息放入ctl_arg.stat中

IPC_SET 用ctl_arg.stat中的值设置所有权/许可权

IPC_RMID 从系统中删除信号量集合

单信号量操作

(下面这些宏与sem_num指定的信号量合semctl返回值相关)

GETVAL 返回信号量的值(也就是semval)

SETVAL 把信号量的值写入ctl_arg.val中

GETPID 返回sempid值

GETNCNT 返回semncnt(参考上面内容)

GETZCNT 返回semzcnt(参考上面内容)

全信号量操作

GETALL 把所有信号量的semvals值写入ctl_arg.array

SETALL 用ctl_arg.array中的值设置所有信号量的semvals

参数arg代表一个union的semun的实例。semun是在linux/sem.h中定义的:

union semun {
int val; //执行SETVAL命令时使用
struct semid_ds *buf; //在IPC_STAT/IPC_SET命令中使用
unsigned short *array; //使用GETALL/SETALL命令时使用的指针
}

联合体中每个成员都有各自不同的类型,分别对应三种不同的semctl 功能,如果semval 是SETVAL.则使用的将是ctl_arg.val.

功能:smctl函数依据command参数会返回不同的值。它的一个重要用途是为信号量赋初值,因为进程无法直接对信号量的值进行修改。

(三)信号量操作semop函数

在 Linux 下,PV 操作通过调用semop函数来实现,也只有它能对PV进行操作

调用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);
返回值:0,如果成功。-1,如果失败:errno=E2BIG(nsops大于最大的ops数目)
EACCESS(权限不够)
EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行)
EFAULT(sops指向的地址无效)
EIDRM(信号量集已经删除)
EINTR(当睡眠时接收到其他信号)
EINVAL(信号量集不存在,或者semid无效)
ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构)
ERANGE(信号量值超出范围)

参数介绍:

第一个参数semid 是信号量集合标识符,它可能是从前一次的semget调用中获得的。


第二个参数是一个sembuf结构的数组,每个 sembuf 结构体对应一个特定信号的操作,sembuf结构在,<sys/sem.h>中定义

struct sembuf{
usign short sem_num;/*信号量索引*/
short sem_op;/*要执行的操作*/
short sem_flg;/*操作标志*/
}

sem_num 存放集合中某一信号量的索引,如果集合中只包含一个元素,则sem_num的值只能为0。

----------------------------------------------------------------------------------------------

Sem_op取得值为一个有符号整数,该整数实际给定了semop函数将完成的功能。包括三种情况:

如果sem_op是负数,那么信号量将减去它的值,对应于p()操作。这和信号量控制的资源有关。如果没有使用IPC_NOWAIT,那么调用进程将进入睡眠状态,直到信号量控制的资源可以使用为止。

如果sem_op是正数,则信号量加上它的值。对应于v()操作。这也就是进程释放信号量控制的资源。

最后,如果sem_op是0,那么调用进程将调用sleep(),直到信号量的值为0。这在一个进程等待完全空闲的资源时使用。

----------------------------------------------------------------------------------------------

sem_flag是用来告诉系统当进程退出时自动还原操作,它维护着一个整型变量semadj(信号灯的计数器),可设置为 IPC_NOWAIT 或 SEM_UNDO 两种状态。只有将 sem_flg 指定为 SEM_UNDO 标志后,semadj (所指定信号量针对调用进程的调整值)才会更新,即减去减去sem_num的值。 此外,如果此操作指定SEM_UNDO,系统更新过程中会撤消此信号灯的计数(semadj)。此操作可以随时进行---它永远不会强制等待的过程。调用进程必须有改变信号量集的权限。


第三个参数是sembuf组成的数组中索引。参数sops指向由sembuf组成的数组,结构数组中的一员。

实验代码:

实验所需头文件:放在/usr/include目录下

//pv.h头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h> #define SEMPERM 0600
#define TRUE 1
#define FALSE 0
typedef union _semun {
int val;
struct semid_ds *buf;
ushort *array;
} semun;

信号量赋初值以及获取信号量标识符函数:

//initsem.c  对信号量赋初值,初值固定为1
#include "pv.h"
int initsem(key_t semkey)
{
int status=,semid; //信号量标识符semid
if ((semid=semget(semkey,,SEMPERM|IPC_CREAT|IPC_EXCL))==-)
{
if (errno==EEXIST) //EEXIST:信号量集已经存在,无法创建
semid=semget(semkey,,); //创建一个信号量
}
else
{
semun arg;
arg.val=; //信号量的初值
status=semctl(semid,,SETVAL,arg); //设置信号量集中的一个单独的信号量的值。
}
if (semid==-||status==-)
{
perror("initsem failed");
return(-);
}
/*all ok*/
return(semid);
}

v操作

//v.c  V操作
#include "pv.h"
int v(int semid)
{
struct sembuf v_buf; v_buf.sem_num=;
v_buf.sem_op=; //信号量加1
v_buf.sem_flg=SEM_UNDO; if (semop(semid, &v_buf, )==-)
{
perror("v(semid)failed");
exit();
}
return();
}

p操作

//p.c  P操作
#include "pv.h"
int p(int semid)
{
struct sembuf p_buf; p_buf.sem_num=;
p_buf.sem_op=-; //信号量减1,注意这一行的1前面有个负号
p_buf.sem_flg=SEM_UNDO; //p_buf = {0,-1,SEM_UNDO};
if (semop(semid, &p_buf, )==-)
{
perror("p(semid)failed");
exit();
}
return();
}

测试函数一(使用PV操作实现三个进程的互斥)

//testsem.c  主程序,使用PV操作实现三个进程的互斥
#include "pv.h"
void handlesem(key_t skey);
main()
{
key_t semkey=0x200;
int i;
for (i=;i<;i++)
{
if (fork()==) //父进程负责产生3个子进程
handlesem(semkey); //子进程中才执行handlesem,做完后就exit。
}
} void handlesem(key_t skey)
{
int semid;
pid_t pid=getpid(); if ((semid=initsem(skey))<)
exit();
printf("进程 %d 在临界资源区之前 \n",pid);
p(semid); //进程进入临界资源区,信号量减少1
printf("进程 %d 在使用临界资源时,停止10s \n",pid); /*in real life do something interesting */
sleep();
printf("进程 %d 退出临界区后 \n",pid); v(semid); //进程退出临界资源区,信号量加1 printf("进程 %d 完全退出\n",pid);
exit();
}

测试结果截图:

测试函数二(实现两个进程交替输出A和B,并在程序中查看信号量的值)

//ab.c  主程序,使用PV操作,两个进程交替输出A和B,实现临界区的互斥访问的基本模型
#include "pv.h"
main()
{
key_t semkey_A=0x200;
key_t semkey_B=0x220;
int semid_A,semid_B;
if ((semid_A=initsem(semkey_A,))<) exit();
if ((semid_B=initsem(semkey_B,))<) exit();
printf("A 进程A的信号量标识符%d,它的初始值为%d\n",
semid_A,semctl(semid_A, , GETVAL));
printf("B 进程B的信号量标识符%d,它的初始值为%d\n",
semid_B,semctl(semid_B, , GETVAL)); if (fork()!=) //父进程先执行
{
int i;
for (i=;i<;i++)
{
p(semid_A);
printf("A 进程A的信号量值为%d\n",semctl(semid_A, , GETVAL));
v(semid_B);
}
}
else
{
int j;
for (j=;j<;j++)
{
p(semid_B);
printf("B 进程B的信号量值为%d\n",semctl(semid_B, , GETVAL));
v(semid_A);
}
}
}

测试结果

实验思考:

(1)信号量一经创建就存在在内存中,这会影响到其他用户及其程序。因此妥善的做法是在程序结束时,若不再需要该信号量,则可以将其从内存中删除,要求实现删除信号量以及输出信号量的值,使用semctl的删除命令就可以了,代码如下:

#include "pv.h"
main()
{
key_t semkey_A=0x200;
key_t semkey_B=0x220;
int semid_A,semid_B;
if ((semid_A=initsem(semkey_A,))<) exit();
if ((semid_B=initsem(semkey_B,))<) exit();
printf("A 进程A的信号量标识符%d,它的初始值为%d\n",
semid_A,semctl(semid_A, , GETVAL));
printf("B 进程B的信号量标识符%d,它的初始值为%d\n",
semid_B,semctl(semid_B, , GETVAL)); if (fork()!=) //父进程先执行
{
int i;
for (i=;i<;i++)
{
p(semid_A);
printf("A 进程A的信号量值为%d\n",semctl(semid_A, , GETVAL));
v(semid_B);
}
}
else
{
int j;
for (j=;j<;j++)
{
p(semid_B);
printf("B 进程B的信号量值为%d\n",semctl(semid_B, , GETVAL));
v(semid_A);
}
} if((semctl(semid_A,,IPC_RMID))<) //删除进程Ad的信号量值,IPC_RMID是删除命令
{
perror("semctl error");
exit();
}
if((semctl(semid_B,,IPC_RMID))<)
{
perror("semctl error");
exit();
}
}

结果截图:

(实验前后信号量的值与标识符都不在。)

实验测试三:用信号量机制解决实际的进程同步问题。有三个进程分别用P1、P2、P3表示,其中P1输出字符A,P2输出字符B,P3输出字符C;现要求三个进程协作完成如下的输出序列:

ABABABCABABABCABABABC…

自己写的代码:

//abc.c  主程序,使用PV操,在实验二的基础上输出ABABABCABABABCABABABC…
#include "pv.h"
main()
{
key_t semkey_A=0x200;
key_t semkey_B=0x220;
key_t semkey_C=0x240;
int semid_A,semid_B,semid_C; if ((semid_A=initsem(semkey_A))<) exit();
if ((semid_B=initsem(semkey_B))<) exit();
if ((semid_C=initsem(semkey_C))<) exit(); printf("A 进程A的信号量%d,它的初始值为%d\n",
semid_A,semctl(semid_A, , GETVAL));
printf("B 进程B的信号量%d,它的初始值为%d\n",
semid_B,semctl(semid_B, , GETVAL));
printf("C 进程B的信号量%d,它的初始值为%d\n",
semid_C,semctl(semid_C, , GETVAL)); int count=;
if (fork()!=) //父进程先执行
{
int i;
for (i=;i<;i++)
{
p(semid_B);
printf("A 进程A的信号量值为%d\n",semctl(semid_A, , GETVAL));
v(semid_A);
}
} else
{
int j;
for (j=;j<;j++)
{
p(semid_A);
printf("B 进程B的信号量值为%d\n",semctl(semid_B, , GETVAL)); count++;
if (count==)
{ v(semid_C); printf("C 进程C的信号量值为%d,couont=%d\n",semctl(semid_C, , GETVAL),count) ;
v(semid_B); count=;}
else
v(semid_B);
}
} if((semctl(semid_A,,IPC_RMID))<) //删除进程A的信号量值,IPC_RMID是删除命令
{
perror("semctl error");
exit();
}
if((semctl(semid_B,,IPC_RMID))<)
{
perror("semctl error");
exit();
}
if((semctl(semid_C,,IPC_RMID))<)
{
perror("semctl error");
exit();
}
}

实验结果截图:

实验分析:

观察到C是出现在第3个B后面的,就在输出B的控制语句里加一个判断就可以了。

自己写出代码后,发觉实验指导书后面给了答案,坑:

//abc.c  主程序,使用PV操作,三个进程分别输出A和B和C
//同步输出格式为:ABABABC-ABABABC-ABABABC-ABABABC-
#include "pv.h"
main()
{
key_t semkey_A=0x200; key_t semkey_B=0x220;
key_t semkey_C=0x260; int semid_A,semid_B,semid_C;
if ((semid_A=initsem(semkey_A,))<) exit();
if ((semid_B=initsem(semkey_B,))<) exit();
if ((semid_C=initsem(semkey_C,))<) exit();
if (fork()>)//父进程
{
if (fork()>) {//父进程
int i;
for (i=;i<;i++)
{
p(semid_A);
printf("A\n");
v(semid_B);
}
}
else {//第二次fork的子进程
int j;
int count=;
for (j=;j<;j++)
{
p(semid_B);
printf("B\n");
count++;
if (count==) {
v(semid_C);
count=;
}
else {
v(semid_A);
}
}
}
}
else//第一次fork的子进程
{
int k;
for (k=;k<;k++)
{
p(semid_C);
printf("C-\n");
v(semid_A);
}
}
}

实验思考:若将输出语句中的“\n”去掉,程序执行会有什么不同,你推测可能是什么原因造成的?

如果去掉\n等相关输出,代码如下:

//abc.c  主程序,使用PV操,在实验二的基础上输出ABABABCABABABCABABABC…
#include "pv.h"
main()
{
key_t semkey_A=0x200;
key_t semkey_B=0x220;
key_t semkey_C=0x240;
int semid_A,semid_B,semid_C; if ((semid_A=initsem(semkey_A))<) exit();
if ((semid_B=initsem(semkey_B))<) exit();
if ((semid_C=initsem(semkey_C))<) exit(); printf("A 进程A的信号量%d,它的初始值为%d\n",
semid_A,semctl(semid_A, , GETVAL));
printf("B 进程B的信号量%d,它的初始值为%d\n",
semid_B,semctl(semid_B, , GETVAL));
printf("C 进程B的信号量%d,它的初始值为%d\n",
semid_C,semctl(semid_C, , GETVAL)); int count=;
if (fork()!=) //父进程先执行
{
int i;
for (i=;i<;i++)
{
p(semid_B);
printf("A");
v(semid_A);
}
} else
{
int j;
for (j=;j<;j++)
{
p(semid_A);
printf("B"); count++;
if (count==)
{ v(semid_C); printf("C") ;
v(semid_B); count=;}
else
v(semid_B);
}
} if((semctl(semid_A,,IPC_RMID))<) //删除进程A的信号量值,IPC_RMID是删除命令
{
perror("semctl error");
exit();
}
if((semctl(semid_B,,IPC_RMID))<)
{
perror("semctl error");
exit();
}
if((semctl(semid_C,,IPC_RMID))<)
{
perror("semctl error");
exit();
}
}

结果截图:

原因分析:

这和缓冲机制有关(参考:这个写的很不错http://www.myexception.cn/linux-unix/1442125.html):
缓冲机制一般分为:全缓冲、行缓冲、无缓冲。

  • 全缓冲:缓冲区满了以后,才发生真正的IO。我们通常用的磁盘文件IO就是这样的。当然你可以调用flush类函数强制刷新缓冲。
  • 行缓冲:缓冲区满了以后或者缓冲区收到一个换行符(表示已输入或输出一行),后才发生真正的IO,比如标准输出和标准输入默认的缓冲机制就是行缓冲。(行缓冲还有一些规则,参考APUE)
  • 无缓冲:立即发生IO,通常标准出错是不带缓冲的。所以建议用输出信息来调试程序时,最后用标准出错IO,以免调试信息延迟输出。

显然这里printf采用的是标准IO,只有当遇到换行符号后,才会输出,如若没有,则父子进程只能一次性输出缓冲区里内容,就会有上面的结果。

参考:

http://www.cnblogs.com/lixiaofei1987/p/3208414.html semop函数详解

http://www.cnblogs.com/hjslovewcl/archive/2011/03/03/2314341.html 信号量介绍

http://blog.chinaunix.net/uid-23193900-id-3221978.html 三个函数的介绍

linux进程同步之信号量的更多相关文章

  1. Linux进程同步之POSIX信号量

    POSIX信号量是属于POSIX标准系统接口定义的实时扩展部分.在SUS(Single UNIX Specification)单一规范中,定义的XSI IPC中也同样定义了人们通常称为System V ...

  2. linux进程同步机制_转

    转自:Linux进程同步机制 具体应用可参考:线程同步       IPC之信号量 为了能够有效的控制多个进程之间的沟通过程,保证沟通过程的有序和和谐,OS必须提供一 定的同步机制保证进程之间不会自说 ...

  3. linux 下的信号量参数

    linux 下的信号量参数 转载自:http://blog.itpub.net/26110315/viewspace-718306/ 信号量是一种锁机制用于协调进程之间互斥的访问临界资源.以确保某种共 ...

  4. [No00003C]操作系统Operating Systems进程同步与信号量Processes Synchronization and Semaphore

    操作系统Operating Systems进程同步与信号量Processes Synchronization and Semaphore 进程合作:多进程共同完成一个任务 从纸上到实际:生产者− − ...

  5. 【转载】Linux的进程间通信-信号量

    原文:Linux的进程间通信-信号量 Linux的进程间通信-信号量 版权声明: 本文章内容在非商业使用前提下可无需授权任意转载.发布. 转载.发布请务必注明作者和其微博.微信公众号地址,以便读者询问 ...

  6. 【Linux】Semaphore信号量线程同步的例子

    0. 信号量 Linux下的信号量和windows下的信号量稍有不同. Windows Windows下的信号量有一个最大值和一个初始值,初始值和最大值可以不同.  而且Windows下的信号量是一个 ...

  7. linux编程之信号量

    一.概念 linux信号量: 允许多个线程同时进入临界区,可以用于进程间的同步. 和互斥锁(mutex)的区别: 互斥锁只允许一个线程进入临界区. 所在头文件: semaphore.h 二.主要函数 ...

  8. Linux多线程--使用信号量同步线程【转】

    本文转载自:http://blog.csdn.net/ljianhui/article/details/10813469 信号量.同步这些名词在进程间通信时就已经说过,在这里它们的意思是相同的,只不过 ...

  9. Linux多线程编程-信号量

    在Linux中.信号量API有两组.一组是多进程编程中的System V IPC信号量.另外一组是我们要讨论的POSIX信号量. 这两组接口类似,但不保证互换.POSIX信号量函数都已sem_开头,并 ...

随机推荐

  1. 优雅的QSignleton (二) MonoSingleton单例实现

    MonoSingleton.cs namespace QFramework.Example { using System.Collections; using UnityEngine; class C ...

  2. iOS 崩溃日志分析(个人总结,最实用)

    iOS 崩溃日志分析(个人总结,最实用) 要分析奔溃日志需要三个文件:crash日志,symbolicatecrash分析工具,.dSYM符号集 0. 在桌面创建一个crash文件夹 1. 需要Xco ...

  3. Nginx从搭建到配置支持HTTPS

    原文地址:https://www.xingkongbj.com/blog/nginx/nginx.html 安装 基础包 ububtu apt-get install build-essential ...

  4. Cantor表

    题目描述 现代数学的著名证明之一是Georg Cantor证明了有理数是可枚举的.他是用下面这一张表来证明这一命题的: 1/1 1/2 1/3 1/4 1/5 - 2/1 2/2 2/3 2/4 - ...

  5. 利用login-path对MySQL安全加固

      Preface       Connection security is  one of the most important safety strategies which we should ...

  6. VM虚拟机网卡LAN区段模拟内网使用教程

    目录   1. 测试环境   2. 设置LAN区段并测试    2.1. 添加LAN区段    2.2. 在虚拟机中设置静态IP地址    2.3. 测试同一LAN区段的主机是否可以联通    2.4 ...

  7. 避免 ‘sudo echo xxxx >’ 时候 出现 “permission denied”

    ➜  ~ echo "/opt/nfs 10.10.10.*(rw,all_squash,sync)">>/etc/exports zsh: permission de ...

  8. lvs集群实现lvs-dr模型和lvs-nat模型

    ipvsadm ipvsadm命令是lvs集群在应用层的管理工具,我们可以通过此ipvsadm来管理lvs的配置,其实现了集群服务管理:增.删.改,集群服务的RS管理:增.删.改以及查看集群状态. 管 ...

  9. Python核心框架tornado的异步协程的2种方式

    什么是异步? 含义 :双方不需要共同的时钟,也就是接收方不知道发送方什么时候发送,所以在发送的信息中就要有提示接收方开始接收的信息,如开始位,同时在结束时有停止位 现象:没有共同的时钟,不考虑顺序来了 ...

  10. html 截图粘粘图片JS

    web前端socket聊天室功能和在线编辑器上传编辑内容的时候经常会需要上传一些图文信息,但是很多编辑器不支持截图粘粘的功能,这里参考了网友分享的可用方法做一个记录. <html> < ...