Linux进程间的通信方式(三)System V 信号量


前言

本文主要探讨 linux 下进程间的通信方式之信号量,最后通过一个简单的示例实现一个父子进程之间操作临界资源使用信号量处理同步与互斥的功能。


一、信号量概念

信号量(Semaphore)的概念最早由荷兰计算机科学家 Dijkstra(迪杰斯特拉)提出,有时又称“信号灯”。信号量本质上是一个计数器,用于协调多个进程(包括但不限于父子进程)对共享数据对象的读写。与其它进程间通信方式不大相同,它不以传送数据为目的,主要是作为一种进程间的同步机制用来保护共享资源。

1.1 信号跟信号量的区别

信号是一种处理异步事件的机制,用于通知进程某个事件的发生。而信号量是一种用于处理进程或者线程间同步与互斥的机制。

1.2 同步跟互斥的区别

同步与互斥机制是计算机系统中用于控制进程或线程对某些特定资源访问的两种机制。

1. 同步的概念
同步指的是多个进程或线程在相互配合完成某项任务时,需要按照某种特定的顺序或条件来执行。也就是说,一个进程或线程的执行可能依赖于另一个进程或线程的执行结果或状态。

2. 互斥的概念
互斥是指某个资源在某一时刻只允许一个进程或线程访问以防止多个进程或线程同时访问共享资源造成的数据不一致或其他问题。

3. 信号量(Semaphore)既可以用于同步也可以用于互斥

当信号量用于同步时,它通常用于协调多个进程或线程的执行顺序,确保它们按照规定的顺序或条件来执行。例如,生产者-消费者问题中的生产者和消费者进程就需要通过信号量来同步它们的执行。

当信号量用于互斥时,它通常用于保护共享资源,确保同一时刻只有一个进程或线程可以访问该资源。例如,在多个进程需要访问同一文件或数据库时,可以使用信号量来实现互斥访问。

信号量互斥逻辑概念图如下

信号量互斥逻辑概念图

信号量同步逻辑概念图如下

在这里插入图片描述

1.4 原子操作概念

原子操作是指一个操作在执行过程中不会被其他线程或任务打断或分割的一个操作。即这个操作要么完全执行成功,要么完全不执行,不会出现部分执行或中间状态。这种特性使得原子操作在多线程或并发执行的情况下,能够保证对共享资源的操作能够正确、完整地进行。

信号量的原子操作特性是实现进程间同步与互斥的关键。通过P操作和V操作来申请和释放信号量资源,信号量的原子操作特性可以确保在任一时刻只有一个进程能够访问共享资源,从而实现了对临界资源的安全访问。

二、信号量的相关操作函数

2.1 ftok 函数(获取一个key值)

函数原型key_t ftok(const char *pathname, int proj_id);
功能基于文件路径和proj_id子序号生成一个键值
参数
pathname: 文件路径
proj_id: 子序号
返回值成功返回一个键值,失败返回-1并设置errno指明错误的原因

2.2 semget函数(创建或者获取信号量)

函数原型int semget(key_t key, int nsems, int semflg);
功能创建或者获取一个信号量集标识符
参数
key: 键值,唯一标识一个信号量集,可取由ftok创建的key值或指定的一个非负整数值
nsems: 指定信号量集中信号量的个数。
semflg: 这是一组标志,用于控制 semget 函数的行为
返回值成功返回一个信号量集标识符,失败返回-1并设置errno指明错误的原因

这里涉及到一个概念,信号量集。什么是信号量集呢?你可以把它理解为一个数组,这个数组里存放一堆信号量,称之为信号量集。
参数 nsems 就是指定信号量集中信号量的个数。我们使用信号量一般的目的就只是用来做进程间的同步,所以我们只需要设置 nsems 为1即可,后续只对这个信号量进行P、V操作。

关于semflg我已经在第一章介绍消息队列的时候就详细讲过了,这里就不再讲了,可以参考一下msgget函数介绍那段。

传送门:Linux进程间的通信方式(一)System V 消息队列

2.3 semctl函数(控制信号量)

函数原型int semctl(int semid, int semnum, int cmd, ...);
功能信号量控制操作,包括创建、删除、获取和设置信号量的初始值以及获取信号量集的信息
参数
semid: 信号量集的标识符,通过 semget 函数获取
semnum: 指定信号量的编号,用于指定在信号量集中的特定信号量。简单理解就是下标
cmd: 指定信号量的操作类型。常用的就两个,IPC_RMID 和 SETVAL
... : 可选的参数,根据 cmd 的不同,传递不同类型的数据。通常是 union semun
返回值成功返回0,失败返回-1并设置errno指明错误的原因

我们先来看看man手册的这段描述

semctl() performs the control operation specified by cmd on the System V semaphore set identified by semid, or on the semnum-th semaphore of that set. (The semaphores in a set are numbered starting at 0.)

翻译过来就是 semctl 函数对一个由 semid 标识的System V信号量集或是该信号量集中第 semnum 个信号量执行 cmd 指定的控制操作指令(一个信号量集中的信号量从0开始编号)

在这里要注意-th的用法,意思就是第几个的意思。

简而言之就是说 semctl 函数既可以对整个信号量集做控制操作,也可以对单个信号量做控制操作。对单个信号量做控制操作的时候可以通过下标指定操作的信号量,要注意的是第一个信号量的下标为 0,这个跟数组是一样的。

结合上面我们对 semget 函数的分析,一般 semctl 函数的用法是这样的

/* 创建或者获取一个信号量集 */
int create_sem(const char *path, int proj_id, int nsems) 
{
    key_t keyval = ftok(path, proj_id);

    int semid = semget(keyval, nsems, IPC_CREAT | IPC_EXCL | 0666);
    if (semid == -1) {
        if (errno == EEXIST) {
            semid = semget(keyval, 0, 0);
            printf("the semaphore set whose key is %d and semid is %d is already exist!\n", keyval, semid);
            return semid;
        } else 
            perror("semget");
    }

    return semid;
}

/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
    union semun su;
    su.val = value;
    if (semctl(semid, semnum, SETVAL, su) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

int main() 
{
    int ret, semid;

    // 创建或者获取一个信号量集
    semid = create_sem(SEM_PATH_NAME, SEM_PROJ_ID, 1);
    if (semid < 0)
        return -1;

    // 初始化信号量值为1
    init_sem(semid, 0, 1);  

	return 0;
}

1. 创建或者获取一个信号量集并获取该信号量集标识符
在这里我们首先创建了一个信号量集并设置信号量的个数为1。
注意:当我们尝试去获取一个已经存在的信号量集的时候,semget 函数的第二第三个参数是被忽略的,因为你可以直接填0。

2. 初始化该信号量集(操作整个信号量集或对指定信号量进行初始值设置)
接着我们设置信号量集中下标为0也就是第一个信号量的初始值为1。
至此就完成了信号量的初始化了,接下来只需要对该信号量进行P、V操作即可。

常用cmd操作指令

指令描述
IPC_RMID删除信号量集
SETVAL设置指定信号量的值

当cmd为IPC_RMID时只需要三个参数

/* 删除信号量集 */
int del_sem(int semid) {
    if (semctl(semid, 0, IPC_RMID) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

当cmd为SETVAL时需要四个参数,第四个参数是一个联合体,定义如下

union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
};

/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
    union semun su;
    su.val = value;
    if (semctl(semid, semnum, SETVAL, su) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

2.4 semop函数(操作信号量)

函数原型int semop(int semid, struct sembuf *sops, size_t nsops);
功能用于对信号量集或信号量的值进行P和V操作
参数
semid: 信号量集合的标识符,通过 semget 函数获取
sops: 指向一个 struct sembuf 结构体数组的指针,表示要对信号量集进行的操作。
nsops:sops 数组中的元素个数,即要执行的操作数
返回值成功返回0,失败返回-1并设置errno指明错误的原因

sembuf结构体的定义如下

struct sembuf {
    unsigned short sem_num;  // 信号量集合中的信号量编号
    short sem_op;            // 操作类型
    short sem_flg;           // 操作标志
};

以下是结构体sembuf中各个成员变量的解析:

  1. sem_num: 信号量集中某个特定信号量的编号,第一个信号量的编号是0。

  2. sem_op: 要执行的操作,一般只会用到两个值-1和1,PV操作均为原子操作。

    P(-1)操作: 尝试获取资源。若获取失败则阻塞等待,若获取成功则将信号量的值减1表示信号量被本进程占用,其他进程不可获取。
    V(+1)操作: 释放资源。若操作完临界资源后需要释放信号量让其他进程抢夺则进行V操作将信号量的值加1表示信号量被释放,其他进程可以抢夺该信号量。

  3. sem_flg: 信号操作标志,下面是一些常见的 sem_flg 值及其含义:

    0: 如果 sem_flg 设置为 0,表示该操作是阻塞的。如果信号量操作无法立即完成(例如,等待一个信号量变为非零),那么调用进程将会被阻塞,直到该操作可以完成。这个设置通常用于确保进程能够正确地同步对共享资源的访问。

    IPC_NOWAIT: 如果设置了 IPC_NOWAIT 标志(通常在 sys/ipc.h 中定义),表示该操作是非阻塞的。如果信号量操作无法立即完成,系统调用将返回错误,而不是让进程等待。

    SEM_UNDO: 如果设置了 SEM_UNDO 标志(通常在 sys/sem.h 中定义),表示在进程终止时自动撤销该信号量操作的效果。这样可以确保信号量在进程异常退出时恢复到之前的状态,防止信号量泄露(semaphore leak)。

操作单个信号量:

/* P 操作(-1)阻塞获取一个信号量 */
int sem_p(int semid, int semnum) {
    struct sembuf sb = {semnum, -1, SEM_UNDO};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

/* V 操作(+1)释放一个信号量 */
int sem_v(int semid, int semnum) {
    struct sembuf sb = {semnum, 1, SEM_UNDO};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

 sem_p(semid, 0);  // P操作,进入临界区
 sem_v(semid, 0);  // V操作,离开临界区

在上面的示例中,每次只对一个信号量进行一个操作。因此,对于每个 P 或 V 操作,nsops 参数为 1,因为 sops 数组中只包含一个 struct sembuf 元素。

操作两个或两个以上信号量

void multiple_operations(int sem_id) {
    struct sembuf sem_ops[2];
    
    // 第一个信号量的P操作
    sem_ops[0].sem_num = 0;
    sem_ops[0].sem_op = -1;
    sem_ops[0].sem_flg = 0;
    
    // 第二个信号量的V操作
    sem_ops[1].sem_num = 1;
    sem_ops[1].sem_op = 1;
    sem_ops[1].sem_flg = 0;

    // 对两个信号量进行操作
    semop(sem_id, sem_ops, 2);
}

在这个示例中,semop 函数的 nsops 参数为 2,因为 sem_ops 数组中有两个元素,分别对信号量集中的两个信号量进行操作。

简而言之,semop 函数的第三个参数指定的是操作数组的大小,而不是信号量集的大小。对于单个 P 或 V 操作,该参数应为 1;对于多个操作,可以根据需要调整该参数。

三、信号量应用示例

3.1 System V信号量应用示例代码

头文件:sem_com.h

#ifndef SEM_COM_H
#define SEM_COM_H

#ifdef __cplusplus
extern "C" {
#endif

#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>

#define SEM_PATH_NAME "/tmp"
#define SEM_PROJ_ID 0x6666

// 错误处理宏
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};

int create_sem(const char *path, int proj_id, int nsems);
int init_sem(int semid, int semnum, int value);
int del_sem(int semid);
int sem_p(int semid, int semnum);
int sem_v(int semid, int semnum);

#ifdef __cplusplus
}
#endif

#endif // !SEM_COM_H

实现文件:sem_com.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include "sem_com.h"

/* 创建或者获取一个信号量集 */
int create_sem(const char *path, int proj_id, int nsems) 
{
    key_t keyval = ftok(path, proj_id);

    int semid = semget(keyval, nsems, IPC_CREAT | IPC_EXCL | 0666);
    if (semid == -1) {
        if (errno == EEXIST) {
            semid = semget(keyval, 0, 0);
            printf("the semaphore set whose key is %d and semid is %d is already exist!\n", keyval, semid);
            return semid;
        } else 
            perror("semget");
    }

    return semid;
}

/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
    union semun su;
    su.val = value;
    if (semctl(semid, semnum, SETVAL, su) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

/* 删除信号量集 */
int del_sem(int semid) {
    if (semctl(semid, 0, IPC_RMID) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

/* P 操作(-1)阻塞获取一个信号量 */
int sem_p(int semid, int semnum) {
    struct sembuf sb = {semnum, -1, 0};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

/* V 操作(+1)释放一个信号量 */
int sem_v(int semid, int semnum) {
    struct sembuf sb = {semnum, 1, 0};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

互斥程序文件:sem_mutex.c

/*
    功能:实现一个父子进程之间操作临界资源使用信号量处理互斥的功能。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 #include <signal.h>
 #include <sys/wait.h>
#include "sem_com.h"

static int g_number = 0;

int main() 
{
    int ret, semid;

    // 创建或者获取一个信号量集
    semid = create_sem(SEM_PATH_NAME, SEM_PROJ_ID, 1);
    if (semid < 0)
        return -1;

    // 初始化信号量值为1
    init_sem(semid, 0, 1);  

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return -1;
    } else if (pid == 0) { /* child */
        printf("I am child,pid:%d,ppid:%d\n", getpid(), getppid());

        for (int i = 0; i < 15; i++) {
            sem_p(semid, 0);  // P操作,进入临界区
            g_number += i;
            printf("child: g_number: %d\n", g_number);
            sleep(1);
            sem_v(semid, 0);  // V操作,离开临界区  
        }

        exit(0);

    } else {
        printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());

        for (int i = 0; i < 10; i++) {
            sem_p(semid, 0);  // P操作,进入临界区
            g_number -= i;
            printf("father: g_number: %d\n", g_number);
            sleep(1);
            sem_v(semid, 0);  // V操作,离开临界区
        }

        /*阻塞等待子进程结束*/
        ret = wait(NULL);

        if (ret == -1) {
            perror("wait");
            return -1;
        }

        del_sem(semid); // 删除信号量
    }
    
    return 0;
}

同步程序文件:sem_sync.c

/*
    功能:实现一个父子进程之间操作临界资源使用信号量处理同步的功能。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 #include <signal.h>
 #include <sys/wait.h>
#include "sem_com.h"

static int g_number = 0;
int g_running = 1;

void sig_handler(int sig) {
    g_running = 0;
}

int main() 
{
    int ret, semid;

    /*创建或者获取一个信号量集并获取一个操作符,信号量个数为2,一个用于互斥,一个用于同步*/
    semid = create_sem(SEM_PATH_NAME, SEM_PROJ_ID, 2);
    if (semid < 0)
        return -1;

     /*初始化信号量sem0值为1,互斥信号量*/
    init_sem(semid, 0, 1);

    /*初始化信号量sem1值为0,同步信号量*/
    init_sem(semid, 1, 0);

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return -1;
    } else if (pid == 0) { /* child */
        printf("I am child,pid:%d,ppid:%d\n", getpid(), getppid());
        signal(SIGUSR1, sig_handler);

        while (g_running) {
            printf("child,pid:%d waiting for a synchronizing signal!\n", getpid());
            sem_p(semid, 1); // P(sem1 -1)操作,进入临界区,阻塞等待同步信号
            g_number += 1;
            printf("child: g_number: %d\n", g_number);
            sleep(1);
            sem_v(semid, 0); // V(sem0 + 1)操作,离开临界区,释放互斥信号
        }

        exit(0);

    } else {
        printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
        sleep(5);
        for (int i = 0; i < 10; i++) {
            sem_p(semid, 0);  // P(sem0 -1)操作,进入临界区,获取互斥信号
            g_number -= i;
            printf("father: g_number: %d\n", g_number);
            sleep(1);
            sem_v(semid, 1);  // V(sem1 +1)操作,离开临界区,释放同步信号
        }

        kill(pid, SIGUSR1); // 发送信号终止子进程
        sleep(1);
        sem_v(semid, 1);  // V(sem1 +1)操作,离开临界区,释放同步信号
        
        /*阻塞等待子进程结束*/
        ret = wait(NULL);
        if (ret == -1) {
            perror("wait");
            return -1;
        }

        del_sem(semid); // 删除信号量
    }
    
    return 0;
}

Makefile文件

# Makefile for compiling sem_mutex and sem_sync

# Compiler
CC = gcc

# Compiler flags
CFLAGS = -Wall -g

# Source files
COMMON_SRC = sem_com.c
MUTEX_SRC = sem_mutex.c
SYNC_SRC = sem_sync.c

# Header files
HEADERS = sem_com.h

# Executable names
MUTEX_EXEC = sem_mutex
SYNC_EXEC = sem_sync

# Build targets
all: $(MUTEX_EXEC) $(SYNC_EXEC)

$(MUTEX_EXEC): $(MUTEX_SRC) $(COMMON_SRC) $(HEADERS)
	$(CC) $(CFLAGS) -g -o $@ $(MUTEX_SRC) $(COMMON_SRC)

$(SYNC_EXEC): $(SYNC_SRC) $(COMMON_SRC) $(HEADERS)
	$(CC) $(CFLAGS) -g -o $@ $(SYNC_SRC) $(COMMON_SRC)

# Clean up
clean:
	rm -f $(MUTEX_EXEC) $(SYNC_EXEC) *.o

3.2 应用示例代码讲解

编译后输出如下

jeff@jeff:~/sem_example$ ./sem_mutex
I am father,pid:2391,ppid:1333
father: g_number: 0
I am child,pid:2392,ppid:2391
child: g_number: 0
father: g_number: -1
child: g_number: 1
father: g_number: -3
child: g_number: 3
father: g_number: -6
child: g_number: 6
father: g_number: -10
child: g_number: 10
father: g_number: -15
child: g_number: 15
father: g_number: -21
child: g_number: 21
father: g_number: -28
child: g_number: 28
father: g_number: -36
child: g_number: 36
father: g_number: -45
child: g_number: 45
child: g_number: 55
child: g_number: 66
child: g_number: 78
child: g_number: 91
child: g_number: 105
jeff@jeff:~/sem_example$
jeff@jeff:~/sem_example$ ./sem_sync
I am father,pid:2395,ppid:1333
I am child,pid:2396,ppid:2395
child,pid:2396 waiting for a synchronizing signal!
father: g_number: 0
child: g_number: 1
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -1
child: g_number: 2
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -3
child: g_number: 3
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -6
child: g_number: 4
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -10
child: g_number: 5
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -15
child: g_number: 6
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -21
child: g_number: 7
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -28
child: g_number: 8
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -36
child: g_number: 9
child,pid:2396 waiting for a synchronizing signal!
father: g_number: -45
child: g_number: 10
jeff@jeff:~/sem_example$

通过 ipcs -s -i [semid] 指令可以查看指定信号量集的所有信息

jeff@jeff:~$ ipcs -a

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x66000002 6          jeff       666        2

jeff@jeff:~$ ipcs -s -i 6

Semaphore Array semid=6
uid=1000         gid=1000        cuid=1000       cgid=1000
mode=0666, access_perms=0666
nsems = 2
otime = Mon Jul  8 03:56:53 2024
ctime = Mon Jul  8 03:56:45 2024
semnum     value      ncount     zcount     pid
0          0          1          0          2399
1          0          0          0          2400

jeff@jeff:~$

其中 semnum 表示信号量的下标,value 表示信号量的值,ncount 表示等待信号量值增加的进程数(即等待P操作的进程数),zcount 表示等待信号量值减少到0的进程数(即等待V操作的进程数),pid 表示为上次修改该信号量值的进程的PID。

总结

信号量(Semaphore)的本质是一个计数器,它既可以用于同步也可以用于互斥。当用于同步时可以设置两个信号量,一个用于互斥,一个用于同步。

  • 53
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值