进程间同步之--信号量

简介:
     信号量分有名和无名信号量。它们的区别和管道及命名管道的区别类似。有名信号量要求创建一个文件,而无名信号量则直接保存在内存中。

一,Posix信号量
Posex信号量接口总结(见下图):
上面一行是有名信号量,可于fifo相类比,其值保存在文件中,可用于进程和线程同步;
下面一行是无名信号量,可与pipe相类比,其值保存在内存中,可用于进程和线程同步;
中间部分,是两者的公用接口。

sem_open()                                sem_close(),sem_unlink()  //有名信号量
         \ |sem_wait(),sem_post()       |/
         / |sem_trywait(),sem_getvalue()|\sem_destroy()  //无名信号量
sem_init()



1.公共接口

1.1 接口函数说明

#include <semaphore.h>
int sem_wait(sem_t *sem);
    测试所指定信号量的值,它的操作是原子的。
    若sem>0,那么它减1并立即返回。
    若sem==0,则睡眠直到sem>0,此时立即减1,然后返回。   

int sem_trywait(sem_t *sem);
    其他的行为和sem_wait一样,除了:
    若sem==0,不是睡眠,而是返回一个错误EAGAIN。       

int sem_post(sem_t *sem);
    把指定的信号量sem的值加1;
    呼醒正在等待该信号量的任意线程。
   
int sem_getvalue(sem_t *sem, int *sval);
    取回信号量sem的当前值,把该值保存到sval中。
    若有1个或更多的线程或进程调用sem_wait阻塞在该信号量上,该函数返回两种值:
        1) 返回0
        2) 返回阻塞在该信号量上的进程或线程数目
    linux采用返回的第一种策略。

注意:在这些函数中,只有sem_post是信号安全的函数,它是可重入函数。

1.2 接口使用的一般流程

sem_init(&sem);
sem_wait(&sem);
critical area;
sem_post(&sem);
remainder area

2.无名信号量
    无名信号量是保存在变量类型为sem_t的内存中。

int sem_init(sem_t *sem, int pshared, unsigned int value);
    1)pshared==0 用于同一多线程的同步;
    2)若pshared>0 用于多个进程间的同步,此时sem必须放在共享内存中。

int sem_destroy(sem_t *sem);
    只能销毁由sem_init初始化的信号量,否则后果不可预料也。

例1:
    多线程使用信号量的简单例子:

/*
 * simple_sem_app.c
 */
#include "all.h"

/* 每个字符输出的间隔时间 */
#define TEN_MILLION 5000000L
#define BUFSIZE 1024

void *threadout(void *args);

int main(int argc, char *argv[])
{
    int error;
       int i;
       int n;
    sem_t semlock;
       pthread_t *tids;
   
       if (argc != 2) {
           fprintf (stderr, "Usage: %s numthreads\n", argv[0]);
              return 1;
       }  
       n = atoi(argv[1]);
       tids = (pthread_t *)calloc(n, sizeof(pthread_t));
       if (tids == NULL) {
           perror("Failed to allocate memory for thread IDs");
           return 1;
       }  
       if (sem_init(&semlock, 0, 1) == -1) {
           perror("Failed to initialize semaphore");
           return 1;
       }  
       for (i = 0; i < n; i++) {
           if (error = pthread_create(tids + i, NULL, threadout, &semlock)) {
               fprintf(stderr, "Failed to create thread:%s\n", strerror(error));
                  return 1;
          }
    }
       for (i = 0; i < n; i++) {
           if (error = pthread_join(tids[i], NULL)) {
               fprintf(stderr, "Failed to join thread:%s\n", strerror(error));
                 return 1;
              }
    }
    return 0;
}

void *threadout(void *args)
{
    char buffer[BUFSIZE];
       char *c;
       sem_t *semlockp;
       struct timespec sleeptime;
   
       semlockp = (sem_t *)args;
       sleeptime.tv_sec = 0;
       sleeptime.tv_nsec = TEN_MILLION;
   
       snprintf(buffer, BUFSIZE, "This is thread from process %ld\n",
               (long)getpid());
       c = buffer;
       /****************** entry section *******************************/
       while (sem_wait(semlockp) == -1)
           if(errno != EINTR) {
               fprintf(stderr, "Thread failed to lock semaphore\n");
                 return NULL;
              }
       /****************** start of critical section *******************/
       while (*c != '\0') {
              fputc(*c, stderr);
              c++;
              nanosleep(&sleeptime, NULL);
       }
       /****************** exit section ********************************/
       if (sem_post(semlockp) == -1)
              fprintf(stderr, "Thread failed to unlock semaphore\n");
       /****************** remainder section ***************************/
       return NULL;
}


说明:该例子来自于usp。
    可以把sem_wait和sme_post调用去掉,看看效果,可以看到出现了交叉输出的情况。
    nanosleep调用只是为了让输出的效果更明显,没有其他意义。

更多的例子见mypxsem/prodcons2-4.c


3. 有名信号量
有名信号量是把信号量的值保存在文件中,所以它可以用于线程也可以用于进程间的同步。
如下面的形式:
sem_t *mutex;
...
mutex = sem_open(pathname, O_CREAT | O_EXCL, FILE_MODE, 0);   
if ((childpid = fork()) == 0) {
    /* child */
    ...
    sem_wait(mutext);
    ...
}
/* parent */
...
sem_post(mutex);
...



3.1 常用函数说明
sem_t *sem_open(const char *name, int oflag,
                mode_t mode, unsigned int value);
    返回一个sem_t类型的指针。该指针随后可用作sem_close等的参数。
    该函数参数的详细信息,可以参考手册。

int sem_close(sem_t *sem);
    关闭sem信号量,并释放资源。   
int sem_unlink(const char *name);
    在所有进程关闭信号量后删除name的信号量

3.2 有名信号量的使用

例子:

/*
 * chainname.c
 */   
#include "my_unpipc.h"

#define BUFSIZE 1024
#define PERMS (mode_t)(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FLAGS (O_CREAT | O_EXCL)

static int getnamed(char *name, sem_t **sem, int val);

int main  (int argc, char *argv[]) {
   char buffer[BUFSIZE];
   char *c;
   pid_t childpid = 0;
   int delay;
   volatile int dummy = 0;
   int i, n;
   sem_t *semlockp;

   if (argc != 4){       /* check for valid number of command-line arguments */
      fprintf (stderr, "Usage: %s processes delay semaphorename\n", argv[0]);
      return 1;
   }
   n = atoi(argv[1]);
   delay = atoi(argv[2]);
   for (i = 1; i < n; i++)
      if ((childpid = fork()) > 0)    /* father break */
         break;
   snprintf(buffer, BUFSIZE,
      "i:%d  process ID:%ld  parent ID:%ld  child ID:%ld\n",
       i, (long)getpid(), (long)getppid(), (long)childpid);
   c = buffer;
   if (getnamed(argv[3], &semlockp, 1) == -1) {
      perror("Failed to create named semaphore");
      return 1;
   }
   while (sem_wait(semlockp) == -1)                         /* entry section */
       if (errno != EINTR) {
          perror("Failed to lock semlock");
          return 1;
       }
   while (*c != '\0') {                                  /* critical section */
      fputc(*c, stderr);
      c++;
      for (i = 0; i < delay; i++)
         dummy++;
   }
   if (sem_post(semlockp) == -1) {                           /* exit section */
      perror("Failed to unlock semlock");
      return 1;
   }
   if (wait(NULL) == -1)                              /* remainder section */
      return 1;
   return 0;
}

static int getnamed(char *name, sem_t **sem, int val)
{
   while (((*sem = sem_open(name, FLAGS , PERMS, val)) == SEM_FAILED) &&
           (errno == EINTR)) ;
   if (*sem != SEM_FAILED)
       return 0;
   if (errno != EEXIST)
      return -1;
   while (((*sem = sem_open(name, 0)) == SEM_FAILED) && (errno == EINTR)) ;
   if (*sem != SEM_FAILED)
       return 0;
   return -1;
}



以上代码创建了一个进程链,若把sem_wait和sem_post调用去掉,可以看到输出很混乱。
这是由于每个子进程都共享了父进程的文件表项,而且都指向打开的文件表项。

system v 信号量
===============
1, 该类信号量,与posix信号量不同。它表示的信号量集,而不是单个信号量。
可用于不同进程间的同步。
内核为每个信号量集,维护一个如下的信息结构:<sys/sem.h>
struct semid_ds {
    struct ipc_perm sem_perm;    /* 信号量集的操作许可权限 */
    struct sem *sem_base;        /* 某个信号量sem结构数组的指针,
                                   当前信号量集中的每个信号量对应其中一个数组元素 */
    ushort sem_nsems;            /* sem_base 数组的个数 */
    time_t sem_otime;            /* 最后一次成功修改信号量数组的时间 */
    time_t sem_ctime;            /* 成功创建时间 */
};

struct sem {
    ushort semval;        /* 信号量的当前值 */
    short  sempid;        /* 最后一次返回该信号量的进程ID号 */
    ushort semncnt;        /* 等待semval大于当前值的进程个数 */
    ushort semzcnt;        /* 等待semval变成0的进程个数 */
};



2, 信号量操作函数
a. 创建和打开信号量
int semget(key_t key, int nsems, int oflag)
(1) nsems>0  : 创建一个信的信号量集,指定集合中信号量的数量,一旦创建就不能更改。
(2) nsems==0 : 访问一个已存在的集合
(3) 返回的是一个称为信号量标识符的整数,semop和semctl函数将使用它。
(4) 创建成功后一下结构被设置:
    .sem_perm 的uid和gid成员被设置成的调用进程的有效用户ID和有效组ID
    .oflag 参数中的读写权限位存入sem_perm.mode
    .sem_otime 被置为0,sem_ctime被设置为当前时间
    .sem_nsems 被置为nsems参数的值
    .而于该集合中的每个信号量不初始化,这些结构是在semctl,用参数SET_VAL,SETALL初始化的。

b. 设置信号量的值
int semop(int semid, struct sembuf *opsptr, size_t nops);
(1) semid 是semget返回的semid
(2) nops : 是数组opsptr的个数
(3) opsptr : 是操作结构的数组

struct sembuf {
    short sem_num;    /* 信号量的数目: 0,1,...,nsems-1 */
    short sem_op;    /* 信号量操作 */
    short sem_flg;  /* 操作表示符 */
};


(4) 若sem_op 是正数,其值就加到semval上;
    若sem_op 是0,那么调用者希望等到semval变为0,如果semval是0就反回;
    若sem_op 是负数,那么调用者希望等待semval变为大于或等于sem_op的绝对值.
(5) sem_flg
    SEM_UNDO     由进程自动释放信号量
    IPC_NOWAIT  不阻塞

c. 对信号量集实行控制操作
int semctl(int semid, int semnum, int cmd, ../* union semun arg */);
其中semid是信号量集合,semnum是信号在集合中的序号,

union semun
{
    int val; /* cmd == SETVAL */
    struct semid_ds *buf /* cmd == IPC_SET或者 cmd == IPC_STAT */
    ushort *array; /* cmd == SETALL, 或 cmd = GETALL */
};



cmd是控制命令,参数可选
cmd取值如下:
GETVAL, SETVAL : semid集合中semnum信号量当前的semval值
GETALL,SETALL :semid集合中所有信号量的值。
IPC_RMID:删除semid信号量集
GETPID:返回最后成功操作该信号的进程号。
IPC_STAT:返回semid集合中的struct semid_ds结构。

例子:

/* my_sem.c */

#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
 
int main (int argc, char **argv)
{
    key_t ipckey;
    int semid;
    /*建立两个信号灯结构*/
    struct sembuf sem[2]; /* sembuf defined in sys/sem.h */
    /* 创建IPC Key */
    ipckey = ftok("/tmp/rich", 42);
    /* 创建信号量. 4 == READ, 2 == ALTER */
    semid = semget(ipckey, 1, 0666 | IPC_CREAT);
    if (semid < 0)  
    {
        printf("Error - %sn", strerror(errno));
        _exit(1);
    }
    /*设置*/
    /* These never change so leave them outside the loop */
    sem[0].sem_num = 0;
    sem[1].sem_num = 0;
    sem[0].sem_flg = SEM_UNDO; /* Release semaphore on exit */
    sem[1].sem_flg = SEM_UNDO; /* Release semaphore on exit */
    while(1)
    {  
        printf("[%s] Waiting for the semaphore to be releasedn\n", argv[1]);
        /* 设置两个信号灯,灯1等待,灯2请求资源锁 */
        sem[0].sem_op = 0; /* Wait for zero */
        sem[1].sem_op = 1; /* Add 1 to lock it*/
        /*设置信号量集,两个信号量*/
        semop(semid, sem, 2);
 
        /*资源锁区*/
        printf("[%s] I have the semaphoren\n", argv[1]);
        sleep(rand() % 3);  
        /* Critical section, sleep for 0-2 seconds */
        sem[0].sem_op = -1; /* Decrement to unlock */
        /*出锁,对信号量1操作*/
        semop(semid, sem, 1);
        printf("[%s] Released semaphoren\n", argv[1]);
        sleep(rand() % 3); /* Sleep 0-2 seconds */
    }
} 



目录
相关文章
|
1月前
|
Python
多进程同步之文件锁
【10月更文挑战第16天】文件锁是一种常用的多进程同步机制,它可以用于确保多个进程在访问共享资源时的互斥性。在使用文件锁时,需要注意锁的粒度、释放、竞争和性能等问题。通过合理使用文件锁,可以提高多进程程序的正确性和性能
|
6月前
|
算法 数据库
操作系统:经典进程同步问题的高级探讨
操作系统:经典进程同步问题的高级探讨
96 1
|
3月前
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
4月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
189 3
|
4月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
80 0
|
4月前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
6月前
|
数据挖掘 调度 开发者
Python并发编程的艺术:掌握线程、进程与协程的同步技巧
并发编程在Python中涵盖线程、进程和协程,用于优化IO操作和响应速度。`threading`模块支持线程,`multiprocessing`处理进程,而`asyncio`则用于协程。线程通过Lock和Condition Objects同步,进程使用Queue和Pipe通信。协程利用异步事件循环避免上下文切换。了解并发模型及同步技术是提升Python应用性能的关键。
134 5
|
5月前
|
Python
在Python中,`multiprocessing`模块提供了一种在多个进程之间共享数据和同步的机制。
在Python中,`multiprocessing`模块提供了一种在多个进程之间共享数据和同步的机制。
|
5月前
|
安全 API Python
`multiprocessing`是Python的一个标准库,用于支持生成进程,并通过管道和队列、信号量、锁和条件变量等同步原语进行进程间通信(IPC)。
`multiprocessing`是Python的一个标准库,用于支持生成进程,并通过管道和队列、信号量、锁和条件变量等同步原语进行进程间通信(IPC)。
|
5月前
|
消息中间件 Linux
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
80 0