【Linux】进程间通信(IPC)之信号量详解与测试用例

2017年03月22日 17:28:50

阅读数:2255

学习环境centos6.5 Linux内核2.6


进程间通信概述

1. 进程通信机制

一般情况下,系统中运行着大量的进程,而每个进程之间并不是相互独立的,有些进程之间经常需要互相传递消息。但是每个进程在系统中都有自己的地址空间,操作系统通过页表和实际物理内存所关联,不允许其他进程随意进入。因此,就必须有一种机制既能保证进程之间的通信,又能保证系统的安全,即进程间通信机制——I P C (Inter_Process Communication)

Linux中的内存空间分为系统空间用户空间。在系统空间中,由于各个线程的地址空间都是共享的,即一个线程能够随意访问kernel中的任意地址,所以无需进程通信机制的保护。而在用户空间中,每个进程都有自己的地址空间,一个进程为了与其他进程通信,必须陷入到有足够权限访问其他进程空间的kernel中,从而与其他进程进行通信。在Linux中支持System V 进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。

2. 进程通信对象标示符和键

在kernel中,对每一类I P C 对象,都由一个非负整数来索引。为了识别并唯一标识各个进程通信的对象,需要一个标识符(即IPC标示符)来标识各个通信对象。而为了获取一个独一无二的通信对象,必须使用(可使用ftok( )函数生成,返回值key)。这里的键是用来定位I P C 对象的标识符的。


背景知识

1. 原子操作(atomic operation)

原子操作意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的。

2. 同步与互斥

同步:在访问资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问。

同步与互斥是保证在高效率运行的同时,可以正确运行。大部分情况下同步是在互斥的基础上进行的。
  • 1
  • 2

3. 临界资源

不同进程能够看到的一份公共的资源(如:打印机,磁带机等),且一次仅允许一个进程使用的资源称为临界资源

4. 临界区

临界区是一段代码,在这段代码中进程将访问临界资源(例如:公用的设备或是存储器),当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。

5. 相关命令

  • ipcs -s 显示已存在的信号量
  • ipcrm -s 删除指定信号量

注意:有时候因为权限问题需要在root下查看与删除。


什么是信号量(Semaphore)

信号量(Semaphore)可以被看做是一种具有原子操作的计数器,它控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数目,常常被当做(lock)来使用,防止一个进程访问另外一个进程正在使用的资源。

信号量本身不具有数据交换的功能,而是控制其他资源来实现进程间通信,在此过程中负责数据操作操作的互斥同步等功能。

简言之:信号量的主要目的是为了保护临界资源。
  • 1
  • 2

1. 为什么要使用信号量

为了防止出现因多个进程同时访问一个共享资源而引发的问题,我们需要一种方法,可以通过生成并使用令牌来授权,在任一时刻只能有一个执行流访问代码的临界区域。而信号量就可以提供这样的一种访问机制,让一个临界区同一时刻只有一个执行流在访问它。

2. 信号量的工作原理

  1. 测试控制该资源的信号量。

  2. 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,,表示一个资源被使用。

  3. 若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程被唤醒,从新进入第1步。

  4. 当进程不再使用由一个信号控制的共享资源时,该信号量值增1,如果有进程正在休眠等待该信号量,则会被唤醒。

为了正确地实现信号量,信号量的操作应是原子操作,所以信号量通常是在内核中实现的。
  • 1

3. Linux的信号量机制

  1. 在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。

  2. 创建信号量(semget)和对信号量赋初值(semctl)分开进行,这是一个弱点,因为不能原子地创建一个信号量集合,并且对该集合中各个信号量赋初值。

  3. 即使没有进程在使用I P C资源,它们仍然是存在的,要时刻防止资源被锁定,避免程序在异常情况下结束时没有解锁资源,可以使用关键字(SEM_UNDO )在退出时恢复信号量值为初始值。


相关函数

1、ftok函数

#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char* path, int id);
  • ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,即IPC关键字

  • path 参数就是你指定的文件名(已经存在的文件名),一般使用当前目录。当产生键时,只使用id参数的低8位。

  • id 是子序号, 只使用8bit (1-255)

  • 返回值:若成功返回键值,若出错返回(key_t)-1

在一般的UNIX实现中,是将文件的索引节点号取出(inode),前面加上子序号的到key_t的返回值

2、semget函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget( key_t key, int nsems, int semflg);
  • 用来创建一个信号集,或者获取已存在的信号集。

  • key: 所创建或打开信号量集的键值(ftok成果执行的返回值)。

  • nsems:创建的信号量集中的信号量个数,该参数只在创建信号量时有效。

  • semflg :调用函数的操作类型,也可用于设置信号量集的访问权限,通过or运算使用。

    • IPC_CREAT | IPC _EXCL | 0666 :一般用于创建,可保证返回一个新的ID,同时制定权限为666
    • IPC_CREAT : 用于获取一个已经存在的ID
  • 返回值:成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:

    • EACESS : 没有访问该信号量集的权限。
    • EEXIST:信号量集已经存在,无法创建。
    • EINVAL:参数nsems的值小于0,或者大于该信号量集的限制,或者是该key关联的信号量以存在,并且nsems的值大于该信号量集的信号量数。
    • ENOENT:信号量集不存在,同时没有使用,IPC_CREAT。
    • ENOMEM:没有足够的内存创建新的信号量集。

3、semctl函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semun, int cmd, ...);
  • 用来初始化信号集,或者删除信号集。

  • semid:信号量集I P C 标识符。

  • semun:操作信号在信号集中的编号,第一个信号的号是0.

  • cmd:在semid指定的信号量集合上执行此命令。

  • 第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union):

union semun
{
int val;
struct semid_ds * buf;
unsigned short * array;
struct seminfo * __buf;
};
  • 第三个参数cmd常用命令:

    • IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中。
    • IPC_RMID:从系统中删除该信号量集合。
    • SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数。
  • 返回值:成功返回一个正数,失败返回-1。

4、 semop函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf * sops, unsigned nsops);
  • 功能:操作一个或一组信号。也可以叫PV操作

  • semid:信号集的识别码,可以通过semget获取。

  • sops:是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf 结构表示如下:

struct sembuf
{
unsigned short sem_num; // 在信号集中的编码 0 , 1, ... nsems-1
short sem_op; //操作 负值或正值
short sem_flg; // IPC_NOWAIT, SEM_UNDO
};
  • sembuf结构体参数说明:

    1. sem_num:操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1。

    2. sem_op:操作信号量

      • 若sem_op 为负(P操作), 其绝对值又大于信号的现有值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权。

      • 若sem_op 为正(V操作), 该值会加到现有的信号内值上。通常用于释放所控制资源的使用权。

      • sem_op的值为0:如果没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时睡眠,直到信号量的值为0;否则进程或线程会返回错误EAGAIN。
    3. sem_flg: 信号操作标识,有如下两种选择:

      • IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。

      • SEM_UNDO:程序结束时(正常退出或异常终止),保证信号值会被重设为semop()调用前的值。避免程序在异常情况下结束时未解锁锁定的资源,早成资源被永远锁定。造成死锁。

  • nsops:信号操作结构的数量,恒大于或等于1.

  • 返回值:成功执行时,都会回0,失败返回-1,并设置errno错误信息。


代码演示

(1)、目的阐述:使用P操作和V操作控制临界区,实现两个进程(父进程与子进程)打印不同的文字,在一个进程进入临界区时,另一进程等待。下面使用的测试是,让子进程在它的临界区打印出(你好:),让父进程打印出(在吗?)。

(2)、在没有使用信号量控制时,打印出来的汉字顺序不是通顺的话。在加入PV操作后,可以保证一个进程打印完,另外一个进程继续打印剩下的汉字,而不会互相交叉。

PV操作成功打印截图示例

无PV操作时打印截图

注:
1. 对于第一次进入临界区时父进程先进入还子进程先进入,与操作系统的进程调度算法有关。
2. 没有在程序中设置跳出循环条件,可以ctrl+c 结束后,用命令去删除信号量集。

代码:

mysem_h


#define _MYSEM_H_ #include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h> // ftok
#include <sys/sem.h>
#include <sys//wait.h> #define PATHNAME "." // ftok 中生成key值 . 表示当前路径
#define PROJ_ID 56 // ftok 中配合PATHNAME 生成唯一key值 int create_sems(int nums); // 创建含有nums个信号量的集合
int get_sems(); // 获取信号量 // 初始化semid对应的信号量集中编号为which的信号量值为value
int init_sems(int semid , int which, int value); int destroy_sems(int semid); // 释放该信号量集 int P(int semid, int which); // 表示分配 信号量值-1
int V(int semid, int which); // 表示释放 信号量值+1 #endif /* _MYSEM_H_ */

mysem_c



// 创建信号量和获取信号量公用函数
static int comm_sem ( int nums , int semflag)
{
// 获取key
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
} int semid = semget(key,nums, semflag );
if( semid < 0)
{
perror("semget");
return -1;
}
return semid;
} int create_sems(int nums) // 创建含有nums个信号量的集合
{
return comm_sem(nums, IPC_CREAT|IPC_EXCL|0666);
} int get_sems() // 获取信号量
{
return comm_sem(0, IPC_CREAT);
} union semun
{
int val; // value for SETVAL
struct semid_ds *buf; // buffer for IPC_STAT & IPC_SET
unsigned short *array; // buffer for GETALL & SELALL
struct seminfo * __buf; // buffer for IPC_INFO
}; // 初始化semid对应的信号量集中编号为which的信号量值为value
int init_sems(int semid , int which, int value)
{
union semun _semun;
_semun.val = value;
int ret = semctl(semid, which, SETVAL,_semun);
if(ret < 0)
{
perror("inin_sem");
return -1;
}
return 0;
} int destroy_sems(int semid) // 释放该信号量集
{
int ret = semctl(semid, 0, IPC_RMID, NULL);
if(ret < 0)
{
perror("rm_sem");
return -1;
}
return 0;
} static int comm_sem_op(int semid, int which, int op)
{
struct sembuf _sembuf;
_sembuf.sem_num = which;
_sembuf.sem_op = op;
_sembuf.sem_flg = 0; // IPC_NOWAIT SEM_UNDO
return semop(semid, &_sembuf, 1);
} int P(int semid, int which) // 表示通过 信号量值-1
{
return comm_sem_op(semid, which , -1);
}
int V(int semid, int which) // 表示释放 信号量值+1
{
return comm_sem_op(semid, which, 1);
}

test_mysem_c

// 加入信号量操作后的程序
#include "mysem.h"
#include "mysem.c"
#include <stdio.h>
#include <unistd.h> int main()
{
int semid = create_sems(10); // 创建一个包含10个信号量的信号集
init_sems(semid, 0, 1); // 初始化编号为 0 的信号量值为1 pid_t id = fork(); // 创建子进程
if( id < 0)
{
perror("fork");
return -1;
}
else if (0 == id)
{// child
int sem_id = get_sems();
while(1)
{
P(sem_id, 0); // 对该信号量集中的0号信号 做P操作
printf("你");
fflush(stdout);
sleep(1);
printf("好");
printf(":");
fflush(stdout);
sleep(1);
V(sem_id, 0);
}
}
else
{// father
while(1)
{
P(semid,0);
printf("在");
sleep(1);
printf("吗");
printf("?");
fflush(stdout);
V(semid, 0);
}
wait(NULL);
} destroy_sems(semid);
return 0;
}
// 未加信号量的测试代码
#include "mysem.h"
#include <stdio.h>
#include <unistd.h> int main()
{ pid_t id = fork(); // 创建子进程
if( id < 0)
{
perror("fork");
return -1;
}
else if (0 == id)
{// child
int sem_id = get_sems();
while(1)
{
printf("你");
fflush(stdout);
sleep(1);
printf("好");
printf(":");
fflush(stdout);
sleep(1);
}
}
else
{// father
while(1)
{
printf("在");
sleep(1);
printf("吗");
printf("?");
fflush(stdout);
}
wait(NULL);
} return 0;
} 附上我的测试代码和截图:
#include"mylib.h"
#include "mylib.c"
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/shm.h> typedef struct msgbuf {
char mtext[1024];
} msgbuf_t; int main()
{
msgbuf_t *shmptr;
int shmid = shmget(10001, sizeof(msgbuf_t), IPC_CREAT | 0644);
shmptr = (msgbuf_t *)shmat(shmid, 0, 0);
int semid = create_sems(10); // 创建一个包含10个信号量的信号集
init_sems(semid, 0, 1); // 初始化编号为 0 的信号量值为1 if(fork() == 0) {
printf("I am process P1:");
int count = 0;
int sem_id = get_sems();
while(1) { P(sem_id, 0); // 对该信号量集中的0号信号 做P操作
if(count >= 10)
break;
else{
printf("P1 sending %d\n", count);
memset(shmptr->mtext, 0, sizeof(shmptr->mtext));
sprintf(shmptr->mtext, "%d", count++);
}
V(sem_id, 0);
sleep(1);
}
V(sem_id, 0); shmdt(shmptr);
exit(0);
} else if(fork() == 0) {
int count=0;
printf("I am process P2:");
int sem_id = get_sems();
while(1){
P(sem_id, 0); if(count>=10)
break;
else
{
printf("received from P1: %s\n", shmptr->mtext);
count++;
}
V(sem_id, 0);
sleep(1);
}
V(semid, 0);
shmdt(shmptr);
exit(0);
} printf("I am father process");
destroy_sems(semid); shmdt(shmptr);
// while(1)
sleep(12);
}

  

最新文章

  1. C++的内存泄漏检测
  2. TortoiseSVN 中 一个 Merge revisions to.. 小坑
  3. TransactionScope 使用记录
  4. UVa 673 平衡的括号
  5. java 克隆
  6. PHP的GD 支持和加载MySQL功能
  7. 使用 Python SimpleHTTPServer 快速共享文件
  8. Web Service 小练习
  9. 链接器工具错误 LNK2011
  10. HW2.2
  11. JVM学习001通过实例总结Java虚拟机的运行机制
  12. Jenkins 学习笔记
  13. JVM笔记7-内存分配与回收策略
  14. [ gczdac ] 20190306 访者必阅
  15. maven 使用 log4j
  16. JMeter中BeanShell Sampler调试分享
  17. Cygwin配置总结
  18. 大数高精度计算库gmp简介
  19. scala-03-list操作
  20. sql语句 这里是取一串数据中的 头 中 尾 几个数据

热门文章

  1. seq2seq keras实现
  2. Js基础知识(五) - 前端性能优化总结
  3. BZOJ 2219 数论之神 (CRT推论+BSGS+原根指标)
  4. vs2015显示代码行数
  5. C# 常用方法——base64字符串转图片
  6. 如何制作纯净的U盘启动盘
  7. Javascript你必须要知道的知识点
  8. 【原创】LUOGU P1808 单词分类
  9. HFUUOJ1023 闷声发大财 概率dp
  10. 免费馅饼~-~ (hdu 1176