前言

在 POSIX 系统中,进程间通信是一个很有意思的话题。

POSIX信号量进程是3种 IPC(Inter-Process Communication) 机制之一,3种 IPC 机制源于 POSIX.1 的实时扩展。Single UNIX Specification 将3种机制(消息队列,信号量和共享存储)置于可选部分中。在 SUSv4 之前,POSIX 信号量接口已经被包含在信号量选项中。在 SUSv4 中,这些接口被移至了基本规范,而消息队列和共享存储接口依然是可选的。

POSIX 信号量接口意在解决 XSI 信号量接口的几个缺陷。

  • 相比于 XSI 接口,POSIX 信号量接口考虑了更高性能的实现。

  • POSIX 信号量使用更简单:没有信号量集,在熟悉的文件系统操作后一些接口被模式化了。尽管没有要求一定要在文件系统中实现,但是一些系统的确是这么实现的。

  • POSIX 信号量在删除时表现更完美。回忆一下,当一个 XSI 信号量被删除时,使用这个信号量标识符的操作会失败,并将 errno 设置成 EIDRM。使用 POSIX 信号量时,操作能继续正常工作直到该信号量的最后一次引用被释放。

    ——摘自《UNIX高级环境编程(中文第3版)》465-466页

前段时间笔者在写管道通信的时候,探究了一下 POSIX 进程间的两种信号量通信方式:有名信号量和无名信号量。有很多人认为进程间通信只能使用有名信号量,无名信号量只能用于单进程间的多线程通信。其实无名信号量也可以进行进程间通信。

区别

有名信号量和无名信号量的差异在于创建和销毁的形式上,但是其他工作一样。

无名信号量只能存在于内存中,要求使用信号量的进程必须能访问信号量所在的这一块内存,所以无名信号量只能应用在同一进程内的线程之间(共享进程的内存),或者不同进程中已经映射相同内存内容到它们的地址空间中的线程(即信号量所在内存被通信的进程共享)。意思是说无名信号量只能通过共享内存访问。

相反,有名信号量可以通过名字访问,因此可以被任何知道它们名字的进程中的线程使用。

单个进程中使用 POSIX 信号量时,无名信号量更简单。多个进程间使用 POSIX 信号量时,有名信号量更简单。

联系

无论是有名信号量还是无名信号量,都可以通过以下函数进行信号量值操作。

wait

weit 为信号量值减一操作,总共有三个函数,函数原型如下:

#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

Link with -pthread.这一句表示 gcc 编译时,要加 -pthread.返回值:若成功,返回 0 ;若出错,返回-1
复制代码

其中,第一个函数的作用是,若 sem 小于 0 ,则线程阻塞于信号量 sem ,直到 sem 大于 0 ;否则信号量值减1。

第二个函数作用与第一个相同,只是此函数不阻塞线程,如果 sem 小于 0,直接返回一个错误(错误设置为 EAGAIN )。

第三个函数作用也与第一个相同,第二个参数表示阻塞时间,如果 sem 小于 0 ,则会阻塞,参数指定阻塞时间长度。 abs_timeout 指向一个结构体,这个结构体由从 1970-01-01 00:00:00 +0000 (UTC) 开始的秒数和纳秒数构成。结构体定义如下:

struct timespec {
               time_t tv_sec;      /* Seconds */
               long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
           };
复制代码

如果指定的阻塞时间到了,但是 sem 仍然小于 0 ,则会返回一个错误 (错误设置为 ETIMEDOUT )。

post

post 为信号量值加一操作,函数原型如下:

#include <semaphore.h>

int sem_post(sem_t *sem);

Link with -pthread.返回值:若成功,返回 0 ;若出错,返回-1
复制代码

应用实例

有名信号量

创建

有名信号量创建可以调用 sem_open 函数,函数说明如下:

#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);  
sem_t *sem_open(const char *name, int oflag,	
                       mode_t mode, unsigned int value);	

Link with -pthread.返回值:若成功,返回指向信号量的指针;若出错,返回SEM_FALLED
复制代码

其中第一种函数是当使用已有的有名信号量时调用该函数,flag 参数设为 0 。

如果要调用第二种函数,flag 参数应设为 O_CREAT ,如果有名信号量不存在,则会创建一个新的,如果存在,则会被使用并且不会再初始化。

当我们使用 O_CREAT 标志时,需要提供两个额外的参数:

mode 参数指定谁可以访问信号量,即权限组,mode 的取值和打开文件的权限位相同,比如 0666 表示 所有用户可读写 。因为只有读和写访问要紧,所以实现经常为读和写打开信号量。

value 指定信号量的初始值,取值范围为 0~SEM_VALUE_MAX 。

如果信号量存在,则调用第二个函数会忽略后面两个参数(即 mode 和 value )。

释放

当完成信号量操作以后,可以调用 sem_close 函数来释放任何信号量的资源。函数说明如下:

#include <semaphore.h>

int sem_close(sem_t *sem);

Link with -pthread.返回值:若成功,返回 0 ;若出错,返回-1
复制代码

如果进程没有调用该函数便退出了,内核会自动关闭任何打开的信号量。无论是调用该函数还是内核自动关闭,都不会改变释放之前的信号量值。

销毁

可以使用 sem_unlink 函数销毁一个有名信号量。函数说明如下:

#include <semaphore.h>

int sem_unlink(const char *name);

Link with -pthread.返回值:若成功,返回 0 ;若出错,返回-1
复制代码

sem_unlink 函数会删除信号量的名字。如果没有打开的信号量引用,则该信号量会被销毁,否则,销毁会推迟到最后一个打开的引用关闭时才进行。

例子

例如,管道通信中,如果父进程使用 fork()创建两个子进程1和2,子进程1,2按顺序向管道写一段文字,最后父进程从管道将子进程写入的内容读出来,要保证进程执行的先后顺序,可以用有名信号量来解决。

#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<semaphore.h>
#include<sys/sem.h>
#include <sys/stat.h>

#include<fcntl.h>

int main(){
    int pid1,pid2;
    sem_t *resource1; 
    sem_t *resource2; 
    int Cpid1,Cpid2=-1;
    int fd[2];//0为读出段,1为写入端
    char outpipe1[100],inpipe[200],outpipe2[100];
    pipe(fd);//建立一个无名管道

    pid1 = fork();
    if(pid1<0){
        printf("error in the first fork!");
    }else if(pid1==0){//子进程1
        resource1=sem_open("name_sem1",O_CREAT,0666,0);
        Cpid1 = getpid();
        close(fd[0]);//关掉读出端
        lockf(fd[1],1,0);//上锁,则锁定从当前偏移量到文件结尾的区域
        sprintf(outpipe1,"Child process 1 is sending a message!");
        write(fd[1],outpipe1,strlen(outpipe2));
        lockf(fd[1],0,0);//解锁
        sem_post(resource1);
        sem_close(resource1);
        exit(0);
   }else{
        
        pid2 = fork();
        if(pid2<0){
            printf("error in the second fork!\n");
        }else if(pid2==0){  
                resource1=sem_open("name_sem1",O_CREAT,0666,0);
                resource2=sem_open("name_sem2",O_CREAT,0666,0);
                Cpid2 = getpid();
                sem_wait(resource1);
				close(fd[0]);
                lockf(fd[1],1,0);
                sprintf(outpipe2,"Child process 2 is sending a message!");

                write(fd[1],outpipe2,strlen(outpipe2));
                lockf(fd[1],0,0);//解锁
                sem_post(resource2);
                sem_close(resource1);
                sem_close(resource2);
                exit(0);
        }
        if(pid1 > 0 && pid2 >0){
                resource2=sem_open("name_sem2",O_CREAT,0666,0);
                sem_wait(resource2);
                waitpid(pid1,NULL,0);
                waitpid(pid2,NULL,0);
                close(fd[1]);//关掉写端
                read(fd[0],inpipe,200);
                printf("%s\n",inpipe);
                sem_close(resource2);
                
                exit(0);
        }
        sem_unlink("name_sem1");
        sem_unlink("name_sem2");
    }
    return 0;
}

复制代码

无名信号量

创建

无名信号量可以通过 sem_init 函数创建,函数说明如下:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

Link with -pthread.返回值:若成功,返回 0 ;若出错,返回-1
复制代码

pshared 参数指示该信号量是被一个进程的多个线程共享还是被多个进程共享。

如果 pshared 的值为 0 ,那么信号量将被单进程中的多线程共享,并且应该位于某个地址,该地址对所有线程均可见(例如,全局变量或变量在堆上动态分配)。

如果 pshared 非零,那么信号量将在进程之间共享,并且信号量应该位于共享内存区域。

销毁

如果无名信号量使用完成,可以调用 sem_destory 函数销毁该信号量。函数说明如下:

#include <semaphore.h>

int sem_destroy(sem_t *sem);

Link with -pthread.返回值:若成功,返回 0 ;若出错,返回-1
复制代码

注意:

  • 销毁其他进程或线程当前被阻塞的信号量会产生未定义的行为。
  • 使用已销毁的信号量会产生未定义的结果,除非使用 sem_init 重新初始化信号量。
  • 一个无名信号量应该在它所在的内存被释放前用 sem_destroy 销毁。如果不这样做,可能会导致某些实现出现资源泄漏。

例子

使用无名信号量实现有名信号量中的例子:

#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<semaphore.h>
#include<sys/sem.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include<fcntl.h>

int main(){
    int pid1,pid2;
    int Cpid1,Cpid2=-1;
    int fd[2];//0为读出段,1为写入端
    char outpipe1[100],inpipe[200],outpipe2[100];
    void *shm = NULL;
    sem_t *shared;
    int shmid = shmget((key_t)(1234), sizeof(sem_t *), 0666 | IPC_CREAT);//创建一个共享内存,返回一个标识符
    if(shmid == -1){
        perror("shmat :");
        exit(0);
    }
    shm = shmat(shmid, 0, 0);//返回指向共享内存第一个字节的指针
    shared = (sem_t *)shm;
    sem_init(shared, 1, 0);//初始化共享内存信号量值为0
    pipe(fd);//建立一个无名管道

    pid1 = fork();
    if(pid1<0){
        printf("error in the first fork!");
    }else if(pid1==0){//子进程1

        Cpid1 = getpid();
        close(fd[0]);//关掉读出端
        lockf(fd[1],1,0);//上锁,则锁定从当前偏移量到文件结尾的区域
        sprintf(outpipe1,"Child process 1 is sending a message!");
        write(fd[1],outpipe1,strlen(outpipe1));
        lockf(fd[1],0,0);//解锁
        sem_post(shared);

        exit(0);
   }else{

        pid2 = fork();
        if(pid2<0){
            printf("error in the second fork!\n");
        }else if(pid2==0){
                sem_wait(shared);
                Cpid2 = getpid();
				close(fd[0]);
                lockf(fd[1],1,0);
                sprintf(outpipe2,"Child process 2 is sending a message!");

                write(fd[1],outpipe2,strlen(outpipe2));
                lockf(fd[1],0,0);//解锁

                exit(0);
        }
        if(pid1 > 0 && pid2 >0){

                waitpid(pid2,NULL,0);//同步,保证子进程先写父进程再读
                close(fd[1]);//关掉写端
                read(fd[0],inpipe,200);
                printf("%s\n",inpipe);

                exit(0);
        }

    }
    return 0;
}