信号量的无序竞争和有序竞争

在linux的多进程(或者多线程,这里以进程为例)开发里经常有进程间的通信部分,常见的技术手段有信号量、消息队列、共享内存等,而共享内存和信号量就像衬衫和外套一样搭配才算完整。

信号量的使用可以使对资源的访问成为独占的,一次只允许相同的进程访问,而所有其他进程都在排队等待或取消回家的行程。

[En]

The use of semaphores can make access to resources exclusive, allowing only the same process to access at a single time, while all other processes wait in line or cancel the trip home.

由于对资源的访问应该是排他性的,因此在获得访问方面必须有竞争。竞争反过来又会使结果井然有序,包括有序和无序。无序意味着竞争是公平的,获得资源的机会是随机的。另一方面,顺序是对比赛结果进行刻意安排,出现固定顺序。例如,在数据生产和消费模式中,数据通常安排为先在生产端输出,然后在消费者端访问。

[En]

Since the access to resources should be exclusive, there must be competition in the acquisition of access. Competition, in turn, will make the results in order, including order and disorder. Disorder means that competition is fair and access to resources is random. On the other hand, the order is that there is a deliberate arrangement for the results of the competition, and a fixed order appears. For example, in the data production and consumption model, the data is generally arranged to be output at the production side first, and then accessed at the consumer side.

好吧,时间太长了。太阳快出来了。

[En]

All right, it’s too long. The sun’s almost out.

信号量的使用库有System V库和POXIS库两种,这里仅简单介绍System V库和相关API,太详细会让人睡着的。

函数原型 备注 int semget(key_t key, int nsems, int semflg) 获取或者创建一个信号量集的标识符,一个信号量集可以包含有多个信号量,nsems代表信号量数量,key可以通过ftok获取(也可以直接使用IPC_PRIVATE,但是仅能用于父子进程间通信),semflg代表信号量集的属性 int semctl(int semid, int semnum, int cmd, union semun arg) 设置或者读取信号量集的某个信号量的信息,semid代表semget返回值,semnum代表信号量的序号,类型union semun在某些系统中不一定存在(如有需要可以自定义) int semop(int semid, struct sembuf *sops, unsigned nsops) 执行PV操作,P是对资源的占用,V是对资源的释放,类型struct sembuf包含了操作的具体内容,nsops代表操作信号量的个数(一般仅用1)

struct sembuf {
    short sem_num;   //指定信号量,信号量在信号量集中的序号,从0开始
    short sem_op;    //小于0,就是执行P操作,对信号量减去sem_op的绝对值;大于0,就是执行V操作,对信号量加上sem_op的绝对值;等于0,等待信号量值归0
    short sem_flg;   //0,IPC_NOWAIT,SEM_UNDO(方便于调用进程崩溃时对信号量值的自动恢复,防止对资源的无用挤占)
}

这里有两种使用信号量的方法。

[En]

Here are two ways to use semaphores.

信号量的无序竞争

信号量最简单的使用方式就是无序的竞争方式。比如在获取资源时,只使用一个信号量,各个进程公平竞争上岗。预设其中一个特定进程启动后,初始化信号量的值为1(调用semctl实现)。然后当所有进程其中的一个需要抢占资源时,P操作对信号量值减1,信号量值归0,调用进程抢占资源成功,资源使用完成后,V操作对信号量值加1,信号量值变为1,释放资源。

当信号量值归0后,其它进程如果需要抢占资源,对信号量执行P操作会导致调用进程挂起并等待,这是调用进程堵塞了。如果执行P操作时,semop的sem_flg用了IPC_NOWAIT,则直接返回-1,通过errno可以获取到错误代码EAGAIN。

PV操作就是通过semop函数对信号量的值检查再加减操作。

我总是觉得,说得太多并不像几行代码那么简单。

[En]

I always feel that talking too much is not as straightforward as a few lines of code.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys sem.h>

void P(int sid)
{
    struct sembuf sem_p;
    sem_p.sem_num = 0;
    sem_p.sem_op = -1;
    sem_p.sem_flg = 0;

    if (semop(sid, &sem_p, 1) == -1) {
        perror("p fail");
        exit(-1);
    }
}

void V(int sid)
{
    struct sembuf sem_v;
    sem_v.sem_num = 0;
    sem_v.sem_op = 1;
    sem_v.sem_flg = 0;

    if (semop(sid, &sem_v, 1) == -1) {
        perror("v fail");
        exit(-1);
    }
}

int main(int argc, char *argv[])
{
    int fd = open("semtest", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        exit(-1);
    }

    key_t key = ftok("semtest", 'a');
    if (key == -1) {
        perror("ftok");
        exit(-1);
    }

    int sid = semget(key, 1, IPC_CREAT | 0666);
    if (sid == -1) {
        perror("semget");
        exit(-1);
    }

    if (semctl(sid, 0, SETVAL, 1) == -1) {
        perror("semctl");
        exit(-1);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(-1);
    } else if (pid == 0) {
        // child process
        while (1) {
            P(sid);
            printf("child get\n");
            sleep(1);
            printf("child release\n");
            V(sid);
        }
    } else {
        // parent process
        printf("parent pid %d child pid %d\n", getpid(), pid);
        while (1) {
            P(sid);
            printf("parent get\n");
            sleep(1);
            printf("parent release\n");
            V(sid);
        }
    }

    return 0;
}
</sys></unistd.h></fcntl.h></stdlib.h></stdio.h>

然后查看结果的输出。这可能是第一次出现这种情况。

[En]

Then look at the output of the result. It may be like this for the first time.

parent pid 13156 child pid 13157
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...

第二次可能就是这样子了

parent pid 12873 child pid 12874
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...

显然,这是信号量无序竞争的结果,就像永远猜不到下一个是汝花姐姐还是白雪公主一样。

[En]

Obviously this is the result of disorderly competition of semaphores, just like never guessing whether the next one will be Sister Ruhua or Snow White.

信号量的有序竞争

事实上,进程之间的资源使用通常是故意顺序的,例如数据生产和消费模型使用场景。当我们去茶馆喝茶时,我们要点菜,等厨房厨师准备好并上菜后才开始吃筷子。这里面有一个固定的顺序。

[En]

In fact, the use of resources between processes is often deliberately sequential, such as data production and consumption model usage scenarios. When we go to the teahouse to have tea, we have to order and wait for the kitchen chefs to prepare and serve it before we start to eat the chopsticks. There is a set order in this.

那么怎么实现信号量的有序操作呢?如果仅仅使用一个信号量,对于各个进程来说,同一个信号量的值,你知我知大家知,大伙处在同一起跑线上,明显一个信号量是不够了。那么可以尝试使用多个信号量,毕竟人多力量大,大力出奇迹?(玩笑,给个评价____)

假设有两个进程(A和B)竞争使用同一个资源,使用资源的顺序要求先是A,然后B,如此循环。每个进程各分配一个代表的信号量(semA/semB)。由于信号量的值默认是0的,那么可以在最优先的进程(A)中对信号量(semA)的值初始化为1,其它信号量(semB)初始化为0,而在其它进程中不需要再对信号量的值作初始化了。

当进程(A)需要抢占资源时,P操作信号量(semA),信号量(semA)的值归0,抢占资源成功。进程(A)使用完需要释放资源时,V操作信号量(semB),信号量(semB)的值变为1,释放完成。在进程(A)中,资源释放后,这时如果再次尝试抢占资源,则P操作信号量(semA),检查信号量(semA)的值,发现已为0,抢占资源失败,进程(A)挂起等待资源。

在进程(A)释放资源后,如果进程(B)尝试抢占资源,P操作信号量(semB),信号量(semB)的值归0,抢占资源成功。进程(B)使用完需要释放资源时,V操作信号量(semA),信号量(semA)的值变为1,释放完成。如果进程(A)未曾抢占资源并且释放,这时进程(B)尝试抢占资源,P操作信号量(semB),检查信号量(semB)的值,发现已为0,抢占资源失败,进程(B)挂起等待资源。

这样就实现了资源总是先给到进程(A),待进程(A)释放资源后,进程(B)才有资格获取到。

下面是代码,look一look

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys sem.h>
#include <string.h>
#include <errno.h>

void P(int sid, int index)
{
    struct sembuf sem_p;
    sem_p.sem_num = index;
    sem_p.sem_op = -1;
    sem_p.sem_flg = 0;

    if (semop(sid, &sem_p, 1) == -1) {
        printf("%d p fail: %s", index, strerror(errno));
        exit(-1);
    }
}

void V(int sid, int index)
{
    struct sembuf sem_v;
    sem_v.sem_num = index;
    sem_v.sem_op = 1;
    sem_v.sem_flg = 0;

    if (semop(sid, &sem_v, 1) == -1) {
        printf("%d v fail: %s", index, strerror(errno));
        exit(-1);
    }
}

int main(int argc, char *argv[])
{
    int fd = open("semtest", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        exit(-1);
    }

    key_t key = ftok("semtest", 'a');
    if (key == -1) {
        perror("ftok");
        exit(-1);
    }

    int sid = semget(key, 2, IPC_CREAT | 0666);
    if (sid == -1) {
        perror("semget 2");
        exit(-1);
    }

    if (semctl(sid, 0, SETVAL, 1) == -1) {
        perror("semctl 0");
        exit(-1);
    }

    if (semctl(sid, 1, SETVAL, 0) == -1) {
        perror("semctl 1");
        exit(-1);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(-1);
    } else if (pid == 0) {
        // child
        while (1) {
            P(sid, 1);
            printf("child get\n");
            sleep(1);
            printf("child release\n");
            V(sid, 0);
        }
    } else {
        // parent
        printf("parent pid %d child pid %d\n", getpid(), pid);
        while (1) {
            P(sid, 0);
            printf("parent get\n");
            sleep(1);
            printf("parent release\n");
            V(sid, 1);
        }
    }

    return 0;
}
</errno.h></string.h></sys></unistd.h></fcntl.h></stdlib.h></stdio.h>

编译执行,看看输出。

parent pid 271 child pid 272
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...

无论执行多少遍这程序,发现parent永远是最先抢占资源的。不信的话,还可以在parent的while循环之前加个延时,再看看输出结果(治好你的小鸡咕噜。。。)。你会发现parent这只小兔子无论故意睡多久的懒觉,还是会第一个冲出屏幕(不是终点线)。

// parent
printf("parent pid %d child pid %d\n", getpid(), pid);
sleep(10);
while (1) {
    P(sid, 0);
    printf("parent get\n");
    sleep(1);
    printf("parent release\n");
    V(sid, 1);
}

如果改变上面的信号量初始化码(自行车会变成摩托车吗?我想得太多了。)

[En]

If the above semaphore initialization code is changed (will the bike become a motorcycle? I think too much.)

更改为:子进程的代表信号量初始化为1,父进程的代表信号量初始化为0。

[En]

Change to: the representative semaphore of the child process is initialized to 1, and the representative semaphore of the parent process is initialized to 0.

if (semctl(sid, 0, SETVAL, 0) == -1) {
    perror("semctl 0");
    exit(-1);
}

if (semctl(sid, 1, SETVAL, 1) == -1) {
    perror("semctl 1");
    exit(-1);
}

编译后再执行程序看看输出,发现最先抢占资源的变成永远是child了

parent pid 298 child pid 299
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...

好了,介绍到这里,期待你的一键三连(⊙o⊙)

Original: https://www.cnblogs.com/englyf/p/16645135.html
Author: englyf八戒
Title: 信号量的无序竞争和有序竞争

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/522775/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球