关于linux:进程间通信之信号量semaphorelinux内核剖析

4次阅读

共计 22760 个字符,预计需要花费 57 分钟才能阅读完成。

什么是信号量


信号量的应用次要是用来爱护共享资源,使得资源在一个时刻只有一个过程(线程)所领有。

信号量的值为正的时候,阐明它闲暇。所测试的线程能够锁定而应用它。若为 0,阐明它被占用,测试的线程要进入睡眠队列中,期待被唤醒。

为了防止出现因多个程序同时拜访一个共享资源而引发的一系列问题,咱们须要一种办法,它能够通过生成并应用令牌来受权,在任一时刻只能有一个执行线程拜访代码的 临界区域

临界区域是指执行数据更新的代码须要独占式地执行。而信号量就能够提供这样的一种拜访机制,让一个临界区同一时间只有一个线程在拜访它,也就是说信号量是用来调协过程对共享资源的拜访的。

信号量是一个非凡的变量,程序对其拜访都是原子操作,且只容许对它进行期待(即 P(信号变量))和发送(即 V(信号变量))信息操作。

最简略的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种模式,叫做二进制信号量。而能够取多个正整数的信号量被称为通用信号量。这里次要探讨二进制信号量。

信号量的工作原理


因为信号量只能进行两种操作期待和发送信号,即 P(sv)和 V(sv), 他们的行为是这样的:

  • P(sv):如果 sv 的值大于零,就给它减 1;如果它的值为零,就挂起该过程的执行
  • V(sv):如果有其余过程因期待 sv 而被挂起,就让它复原运行,如果没有过程因期待 sv 而挂起,就给它加 1.

举个例子,就是两个过程共享信号量 sv,一旦其中一个过程执行了 P(sv)操作,它将失去信号量,并能够进入临界区,使 sv 减 1。而第二个过程将被阻止进入临界区,因为当它试图执行 P(sv)时,sv 为 0,它会被挂起以期待第一个过程来到临界区域并执行 V(sv)开释信号量,这时第二个过程就能够复原执行。

信号量的分类


在学习信号量之前,咱们必须先晓得——Linux 提供两种信号量:

  • 内核信号量,由内核管制门路应用
  • 用户态过程应用的信号量,这种信号量又分为 POSIX 信号量和 SYSTEM V 信号量。

POSIX 信号量又分为 有名信号量 无名信号量 有名信号量 ,其值保留在文件中, 所以它能够用于线程也能够用于过程间的同步。 无名信号量,其值保留在内存中。

POSIX 信号量与 SYSTEM V 信号量的比拟


  1. 对 POSIX 来说,信号量是个非负整数。罕用于线程间同步。
    而 SYSTEM V 信号量则是一个或多个信号量的汇合,它对应的是一个信号量构造体,这个构造体是为 SYSTEM V IPC 服务的,信号量只不过是它的一部分。罕用于过程间同步。
  2. POSIX 信号量的援用头文件是 <semaphore.h>,而 SYSTEM V 信号量的援用头文件是 <sys/sem.h>
  3. 从应用的角度,System V 信号量是简单的,而 Posix 信号量是简略。比方,POSIX 信号量的创立和初始化或 PV 操作就很十分不便。

内核信号量


Linux 内核的信号量在概念和原理上与用户态的 System V 的 IPC 机制信号量是一样的,然而它绝不可能在内核之外应用,它是一种睡眠锁。

如果有一个工作想要取得曾经被占用的信号量时,信号量会将其放入一个期待队列(它不是站在里面痴痴地期待而是将本人的名字写在工作队列中)而后让其睡眠。

当持有信号量的过程将信号开释后,处于期待队列中的一个工作将被唤醒(因为队列中可能不止一个工作),并让其取得信号量。

这一点与自旋锁不同,处理器能够去执行其它代码。

它与自旋锁的差别:因为争用信号量的过程在期待锁从新变为可用时会睡眠,所以 信号量实用于锁会被长时间持有的状况

相同,锁被短时间持有时,应用信号量就不太合适了,因为睡眠、保护期待队列以及唤醒所破费的开销可能比锁占用的全副时间表还要长;

因为执行线程在锁被争用时会睡眠,所以只能在过程上下文中能力取得信号量锁,因为在中断上下文中是不能进行调试的;持有信号量的进行也能够去睡眠,当然也能够不睡眠,因为当其余过程争用信号量时不会因而而死锁;不能同时占用信号量和自旋锁,因为自旋锁不能够睡眠而信号量锁能够睡眠。绝对而来说信号量比较简单,它不会禁止内核抢占,持有信号量的代码能够被抢占。

信号量还有一个特色,就是它容许多个持有者,而自旋锁在任何时候只能容许一个持有者。

当然咱们常常遇到也是只有一个持有者,这种信号量叫二值信号量或者叫互斥信号量。容许有多个持有者的信号量叫计数信号量,在初始化时要阐明最多容许有多少个持有者(Count 值)
信号量在创立时须要设置一个初始值,示意同时能够有几个工作能够拜访该信号量爱护的共享资源,初始值为 1 就变成互斥锁(Mutex),即同时只能有一个工作能够拜访信号量爱护的共享资源。
当工作拜访完被信号量爱护的共享资源后,必须开释信号量,开释信号量通过把信号量的值加 1 实现,如果信号量的值为非负数,表明有工作期待以后信号量,因而它也唤醒所有期待该信号量的工作。

须要 C /C++ Linux 服务器架构师学习材料加群 812855908(材料包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),收费分享

内核信号量的形成


内核信号量相似于自旋锁,因为当锁敞开着时,它不容许内核管制门路持续进行。然而,当内核管制门路试图获取内核信号量锁爱护的忙资源时,相应的过程就被挂起。只有在资源被开释时,过程才再次变为可运行。
只有能够睡眠的函数能力获取内核信号量;中断处理程序和可提早函数都不能应用内核信号量。
内核信号量是 struct semaphore 类型的对象,在内核源码中位于 includelinuxsemaphore.h 文件

struct semaphore
{
   atomic_t count;
   int sleepers;
   wait_queue_head_t wait;
}

内核信号量中的期待队列


下面曾经提到了内核信号量应用了期待队列 wait_queue 来实现阻塞操作。

当某工作因为没有某种条件没有失去满足时,它就被挂到期待队列中睡眠。当条件失去满足时,该工作就被移出期待队列,此时并不意味着该工作就被马上执行,因为它又被移进工作队列中期待 CPU 资源,在适当的机会被调度。

内核信号量是在外部应用期待队列的,也就是说该期待队列对用户是暗藏的,毋庸用户干预。由用户真正应用的期待队列咱们将在另外的篇章进行详解。

内核信号量的相干函数


初始化


#define __SEMAPHORE_INITIALIZER(name, n)                                
{.lock           = __SPIN_LOCK_UNLOCKED((name).lock),            
        .count          = n,                                            
        .wait_list      = LIST_HEAD_INIT((name).wait_list),             
}

该宏申明一个信号量 name 是间接将构造体中 count 值设置成 n,此时信号量可用于实现过程间的互斥量。

#define DECLARE_MUTEX(name)     
        struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

该宏申明一个互斥锁 name,但把它的初始值设置为 1

void sema_init (struct semaphore *sem, int val);

该函用于数初始化设置信号量的初值,它设置信号量 sem 的值为 val。

#define init_MUTEX(sem)         sema_init(sem, 1)

该函数用于初始化一个互斥锁,即它把信号量 sem 的值设置为 1。

#define init_MUTEX_LOCKED(sem)  sema_init(sem, 0)

该函数也用于初始化一个互斥锁,但它把信号量 sem 的值设置为 0,即一开始就处在已锁状态。

留神:对于信号量的初始化函数 Linux 最新版本存在变动,如 init_MUTEX 和 init_MUTEX_LOCKED 等初始化函数目前新的内核中曾经没有或者更换了了名字等

因而倡议当前在编程中遇到须要应用信号量的时候尽量采纳 sema_init(struct semaphore *sem, int val)函数,因为这个函数就目前为止从未发生变化。

获取信号量–申请内核信号量所爱护的资源


void down(struct semaphore * sem);

该函数用于取得信号量 sem,它 会导致睡眠,因而不能在中断上下文(包含 IRQ 上下文和 softirq 上下文)应用该函数。该函数将把 sem 的值减 1,如果信号量 sem 的值非负,就间接返回,否则调用者将被挂起,直到别的工作开释该信号量能力持续运行。

int down_interruptible(struct semaphore * sem);

该函数性能与 down 相似,不同之处为,down 不会被信号(signal)打断,但 down_interruptible 能被信号(比方 Ctrl+C)打断,因而该函数有返回值来辨别是失常返回还是被信号中断,如果返回 0,示意取得信号量失常返回,如果被信号打断,返回 -EINTR

int down_trylock(struct semaphore * sem);

该函数试着取得信号量 sem,如果可能立即取得,它就取得该信号量并返回 0,否则,示意不能取得信号量 sem,返回值为非 0 值。因而,它 不会导致调用者睡眠,能够在中断上下文应用

开释内核信号量所爱护的资源


void up(struct semaphore * sem);

该函数开释信号量 sem,即把 sem 的值加 1,如果 sem 的值为非负数,表明有工作期待该信号量,因而唤醒这些期待者。

内核信号量的应用例程


在驱动程序中,当多个线程同时拜访雷同的资源时(驱动中的全局变量时一种典型的
共享资源),可能会引发“竞态“,因而咱们必须对共享资源进行并发管制。Linux 内核中
解决并发管制的最罕用办法是自旋锁与信号量(绝大多数时候作为互斥锁应用)。

ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
 // 取得信号量
 if (down_interruptible(&sem))
 {return - ERESTARTSYS;}
 // 将用户空间的数据复制到内核空间的 global_var
 if (copy_from_user(&global_var, buf, sizeof(int)))
 {up(&sem);
  return - EFAULT;
 }
 // 开释信号量
 up(&sem);
 return sizeof(int);
}

读 - 写信号量


跟自旋锁一样,信号量也有辨别读 - 写信号量之分

如果一个读写信号量以后没有被写者领有并且也没有写者期待读者开释信号量,那么任何读者都能够胜利取得该读写信号量;

否则,读者必须被挂起直到写者开释该信号量。如果一个读写信号量以后没有被读者或写者领有并且也没有写者期待该信号量,那么一个写者能够胜利取得该读写信号量,否则写者将被挂起,直到没有任何访问者。因而,写者是排他性的,独占性的。

读写信号量有两种实现,一种是通用的,不依赖于硬件架构,因而,减少新的架构不须要从新实现它,但毛病是性能低,取得和开释读写信号量的开销大;另一种是架构相干的,因而性能高,获取和开释读写信号量的开销小,但减少新的架构须要从新实现。在内核配置时,能够通过选项去管制应用哪一种实现。
读写信号量的相干 API 有:

DECLARE_RWSEM(name)

该宏申明一个读写信号量 name 并对其进行初始化。

void init_rwsem(struct rw_semaphore *sem);

该函数对读写信号量 sem 进行初始化。

void down_read(struct rw_semaphore *sem);

读者调用该函数来失去读写信号量 sem。该函数会导致调用者睡眠,因而只能在过程上下文应用。

int down_read_trylock(struct rw_semaphore *sem);

该函数相似于 down_read,只是它不会导致调用者睡眠。它尽力失去读写信号量 sem,如果可能立刻失去,它就失去该读写信号量,并且返回 1,否则示意不能立即失去该信号量,返回 0。因而,它也能够在中断上下文应用。

void down_write(struct rw_semaphore *sem);

写者应用该函数来失去读写信号量 sem,它也会导致调用者睡眠,因而只能在过程上下文应用。

int down_write_trylock(struct rw_semaphore *sem);

该函数相似于 down_write,只是它不会导致调用者睡眠。该函数尽力失去读写信号量,如果可能立即取得,就取得该读写信号量并且返回 1,否则示意无奈立即取得,返回 0。它能够在中断上下文应用。

void up_read(struct rw_semaphore *sem);

读者应用该函数开释读写信号量 sem。它与 down_read 或 down_read_trylock 配对应用。如果 down_read_trylock 返回 0,不须要调用 up_read 来开释读写信号量,因为基本就没有取得信号量。

void up_write(struct rw_semaphore *sem);

写者调用该函数开释信号量 sem。它与 down_write 或 down_write_trylock 配对应用。如果 down_write_trylock 返回 0,不须要调用 up_write,因为返回 0 示意没有取得该读写信号量。

void downgrade_write(struct rw_semaphore *sem);

该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因而在写者放弃读写信号量期间,任何读者或写者都将无法访问该读写信号量爱护的共享资源,对于那些以后条件下不须要写访问的写者,降级为读者将,使得期待拜访的读者可能立即拜访,从而减少了并发性,进步了效率。
读写信号量适于在读多写少的状况下应用,在 linux 内核中对过程的内存映像形容构造的拜访就应用了读写信号量进行爱护。
到底什么时候应用自旋锁什么时候应用信号量,上面给出倡议的计划
当对低开销、短期、中断上下文加锁,优先思考自旋锁;当对长期、持有锁须要休眠的工作,优先思考信号量。

POSIX 信号量详解


无名信号量


无名信号量 的创立就像申明个别的变量一样简略,例如:sem_t sem_id。而后再初始化该无名信号量,之后就能够放心使用了。

无名信号量罕用于多线程间的同步,同时也用于相干过程间的同步。也就是说,无名信号量必须是多个过程(线程)的共享变量,无名信号量要爱护的变量也必须是多个过程(线程)的共享变量,这两个条件是缺一不可的。

常见的无名信号量相干函数


int sem_init(sem_t *sem, int pshared, unsigned int value);
  • pshared==0 用于同一多线程的同步;
  • 若 pshared>0 用于多个相干过程间的同步(即由 fork 产生的)
int sem_getvalue(sem_t *sem, int *sval);

取回信号量 sem 的以后值,把该值保留到 sval 中。
若有 1 个或更多的线程或过程调用 sem_wait 阻塞在该信号量上,该函数返回两种值:

  • 返回 0
  • 返回阻塞在该信号量上的过程或线程数目

linux 采纳返回的第一种策略。

sem_wait(或 sem_trywait)相当于 P 操作,即申请资源。

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。

sem_post 相当于 V 操作,开释资源。

int sem_post(sem_t *sem);

把指定的信号量 sem 的值加 1;

呼醒正在期待该信号量的任意线程。
留神:在这些函数中,只有 sem_post 是信号平安的函数,它是可重入函数

无名信号量在多线程间的同步


无名信号量的常见用法是将要爱护的变量放在 sem_wait 和 sem_post 两头所造成的
临界区内,这样该变量就会被爱护起来,例如:

#include <pthread.h>
#include <semaphore.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int number; // 被爱护的全局变量
sem_t sem_id;

void* thread_one_fun(void *arg) {sem_wait(&sem_id);
    printf("thread_one have the semaphoren");
    number++;

    printf("thread_one : number = %dn", number);

    sem_post(&sem_id);

    return NULL;
}

void* thread_two_fun(void *arg) {sem_wait(&sem_id);
    printf("thread_two have the semaphore n");

    number--;
    printf("thread_two : number = %dn", number);

    sem_post(&sem_id);

    return NULL;
}
int main(int argc, char *argv[]) {
    number = 1;
    pthread_t id1, id2;

    sem_init(&sem_id, 0, 1);

    pthread_create(&id1, NULL, thread_one_fun, NULL);
    pthread_create(&id2, NULL, thread_two_fun, NULL);

    pthread_join(id1, NULL);
    pthread_join(id2, NULL);

    printf("main...n");

    return 0;
}

下面的例程,到底哪个线程先申请到信号量资源,这是随机的。

过程 1 先执行,进城 2 后执行

过程 2 先执行,进城 1 后执行

如果想要某个特定的程序的话,能够用 2 个信号量来实现。例如上面的例程是线程 1 先执行完,而后线程 2 才继续执行,直至完结。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int number;                 // 被爱护的全局变量
sem_t sem_id1, sem_id2;

/*
 *  线程 1,
 *  对 sem_id1 加锁 (P 操作) 当前
 *  将 number 减少 1
 *  同时对 sem_id2 进行开释,V 操作
 *
 *  */
void* thread_one_fun(void *arg) {sem_wait(&sem_id1);
    printf("thread_one have the semaphoren");

    number++;

    printf("number = %dn",number);
    sem_post(&sem_id2);

    return NULL;
}

/*
 *  线程 2,
 *  对 sem_id2 加锁 (P 操作) 当前
 *  将 number 缩小 1
 *  同时对 sem_id1 进行开释,V 操作
 *
 *  */
void* thread_two_fun(void *arg) {sem_wait(&sem_id2);
    printf("thread_two have the semaphore n");

    number--;

    printf("number = %dn",number);
    sem_post(&sem_id1);

    return NULL;
}

int main(int argc,char *argv[]) {
    number = 1;
    pthread_t id1, id2;

    /*
     *  因为程序初始时, sem_id1 可进入, sem_id2 不可进入
     *  两个线程的动作如下:
     *  thread one P(id1)  number++ V(id2)
     *  thread two P(id2)  number-- V(id1)
     *  而 id1 可进入, id2 不可进入
     *  因而 thread one 先执行
     *  如果将 id1 与 id2 的程序替换, 则执行程序相同
     * */
    sem_init(&sem_id1, 0, 1);   // 闲暇的
    sem_init(&sem_id2, 0, 0);   // 忙的

    pthread_create(&id1, NULL, thread_one_fun, NULL);
    pthread_create(&id2, NULL, thread_two_fun, NULL);

    pthread_join(id1, NULL);
    pthread_join(id2, NULL);

    printf("main...n");


    return EXIT_SUCCESS;
}

无名信号量在相干过程间的同步


说是相干过程,是因为本程序中共有 2 个过程,其中一个是另外一个的子过程(由 fork 产生)的。

原本对于 fork 来说,子过程只继承了父过程的代码正本,mutex 理当在父子过程中是互相独立的两个变量,但因为在初始化 mutex 的时候,由 pshared = 1 指定了 mutex 处于共享内存区域,所以此时 mutex 变成了父子过程共享的一个变量。此时,mutex 就能够用来同步相干过程了。

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

#include <semaphore.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char **argv) {
    int     fd, i;
    int     nloop = 10, zero = 0;
    int     *ptr;
    sem_t   mutex;

    //  open a file and map it into memory
    fd = open("log.txt", O_RDWR | O_CREAT, S_IRWXU);

    write(fd,&zero,sizeof(int));

    ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    close(fd);

    /* create, initialize semaphore */
    if(sem_init(&mutex, 1, 1) < 0) //
    {perror("semaphore initilization");
        exit(0);
    }

    if (fork() == 0)
    {   /* child process*/
        for (i = 0; i < nloop; i++)
        {sem_wait(&mutex);

            printf("child: %dn", (*ptr)++);
            //sleep(1);
            sem_post(&mutex);
        }
        exit(0);
    }


    /* back to parent process */
    for (i = 0; i < nloop; i++)
    {sem_wait(&mutex);

        printf("parent: %dn", (*ptr)++);
        //sleep(1);
        sem_post(&mutex);
    }
    exit(0);
}

有名信号量


有名信号量的特点是把信号量的值保留在文件中。

这决定了它的用处十分广:既能够用于线程,也能够用于相干过程间,甚至是不相干过程。

有名信号量能在过程间共享的起因


因为有名信号量的值是保留在文件中的,所以对于相干过程来说,子过程是继承了父过程的文件描述符,那么子过程所继承的文件描述符所指向的文件是和父过程一样的,当然文件外面保留的有名信号量值就共享了。

有名信号量相干函数阐明


有名信号量在应用的时候,和无名信号量共享 sem_wait 和 sem_post 函数。
区别是有名信号量应用 sem_open 代替 sem_init,另外在完结的时候要像敞开文件一样去敞开这个有名信号量。

  • 关上一个已存在的有名信号量,或创立并初始化一个有名信号量。一个繁多的调用就完
    成了信号量的创立、初始化和权限的设置。
sem_t *sem_open(const char *name, int oflag, mode_t mode , int value);

参数形容 name 文件的路径名;Oflag 有 O_CREAT 或 O_CREATmode_t 管制新的信号量的拜访权限;Value 指定信号量的初始化值。

留神:

这里的 name 不能写成 /tmp/aaa.sem 这样的格局,因为在 linux 下,sem 都是创立在 /dev/shm 目录下。你能够将 name 写成“/mysem”或“mysem”,创立进去的文件都是“/dev/shm/sem.mysem”,千万不要写门路。也千万不要写“/tmp/mysem”之类的。

当 oflag = O_CREAT 时,若 name 指定的信号量不存在时,则会创立一个,而且前面的 mode 和 value 参数必须无效。若 name 指定的信号量已存在,则间接关上该信号量,

同时疏忽 mode 和 value 参数。

当 oflag = O_CREAT|O_EXCL 时,若 name 指定的信号量已存在,该函数会间接返回 error。

  • 一旦你应用了一信号量,销毁它们就变得很重要。
    在做这个之前,要确定所有对这个有名信号量的援用都曾经通过 sem_close()函数敞开了,而后只需在退出或是退出处理函数中调用 sem_unlink()去删除零碎中的信号量,留神如果有任何的处理器或是线程援用这个信号量,sem_unlink()函数不会起到任何的作用。

也就是说,必须是最初一个应用该信号量的过程来执行 sem_unlick 才无效。因为每个信号灯有一个援用计数器记录以后的关上次数,sem_unlink 必须期待这个数为 0 时能力把 name 所指的信号灯从文件系统中删除。也就是要期待最初一个 sem_close 产生。

有名信号量在无相干过程间的同步


后面曾经说过,有名信号量是位于共享内存区的,那么它要爱护的资源也必须是位于共享内存区,只有这样能力被无相干的过程所共享。
在上面这个例子中,服务过程和客户过程都应用 shmget 和 shmat 来获取得一块共享内存资源。而后利用有名信号量来对这块共享内存资源进行互斥爱护。

服务器程序

//server.c
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SHMSZ 27

char SEM_NAME[]= "vik";

int main() {
    char    ch;
    int     shmid;
    key_t   key;
    char    *shm,*s;
    sem_t   *mutex;

    //name the shared memory segment
    key = 1000;

    //create & initialize semaphore
    mutex = sem_open(SEM_NAME, O_CREAT, 0644, 1);
    if(mutex == SEM_FAILED)
    {perror("unable to create semaphore");
        sem_unlink(SEM_NAME);

        exit(-1);
    }

    //create the shared memory segment with this key
    shmid = shmget(key, SHMSZ, IPC_CREAT | 0666);
    if(shmid < 0)
    {perror("failure in shmget");

        exit(-1);
    }

    //attach this segment to virtual memory
    shm = shmat(shmid, NULL, 0);
    //start writing into memory
    s = shm;
    for(ch = 'A'; ch <= 'Z'; ch++)
    {sem_wait(mutex);

        *s++ = ch;

        sem_post(mutex);
    }

    //the below loop could be replaced by binary semaphore
    while(*shm != '*')
    {sleep(1);
    }

    sem_close(mutex);

    sem_unlink(SEM_NAME);

    shmctl(shmid, IPC_RMID, 0);

    return EXIT_SUCCESS;

}

客户端程序

// client.c
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SHMSZ 27

char SEM_NAME[]= "vik";

int main() {
    int     shmid;
    key_t   key;
    char    *shm, *s;
    sem_t   *mutex;

    //  name the shared memory segment
    key = 1000;

    //  create & initialize existing semaphore
    mutex = sem_open(SEM_NAME, 0, 0644, 0);
    if(mutex == SEM_FAILED)
    {perror("reader:unable to execute semaphore");
        sem_close(mutex);

        exit(-1);
    }

    //  create the shared memory segment with this key
    shmid = shmget(key, SHMSZ, 0666);
    if(shmid < 0)
    {perror("reader:failure in shmget");

        exit(-1);
    }

    //  attach this segment to virtual memory
    shm = shmat(shmid, NULL, 0);

    //  start reading
    s = shm;
    for(s = shm; *s != '0'; s++)
    {sem_wait(mutex);

        putchar(*s);

        sem_post(mutex);
    }

    //  once done signal exiting of reader:This can be replaced by another semaphore
    *shm = '*';
    sem_close(mutex);
    shmctl(shmid, IPC_RMID, 0);

    return EXIT_SUCCESS;
}

SYSTEM V 信号量


这是信号量值的汇合,而不是单个信号量。相干的信号量操作函数由 <sys/ipc.h> 援用。

ystem V 信号量在内核中保护,其中包含二值信号量、计数信号量、计数信号量集。

  • 二值信号量:其值只有 0、1 两种抉择,0 示意资源被锁,1 示意资源可用;
  • 计数信号量:其值在 0 和某个限定值之间,不限定资源数只在 0 1 之间;
  • 计数信号量集:多个信号量的汇合组成信号量集

信号量构造体


内核为每个信号量集保护一个信号量构造体,可在

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; /* 胜利创立工夫 */
};

其中 ipc_perm 构造是内核给每个过程间通信对象保护的一个信息结构,其成员蕴含所有者用户 id,所有者组 id、创建者及其组 id,以及拜访模式等;semid_ds 构造体中的 sem 构造是内核用于保护某个给定信号量的一组值的内部结构,其构造定义:

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

常见的 SYSTEM V 信号量函数


关键字和描述符


SYSTEM V 信号量是 SYSTEM V IPC(即 SYSTEM V 过程间通信)的组成部分,其余的有 SYSTEM V 音讯队列,SYSTEM V 共享内存。而关键字和 IPC 描述符无疑是它们的共同点,也应用它们,就不得不先对它们进行相熟。这里只对 SYSTEM V 信号量进行探讨。

IPC 描述符相当于援用 ID 号,要想应用 SYSTEM V 信号量(或 MSG、SHM),就必须用 IPC 描述符来调用信号量。而 IPC 描述符是内核动静提供的(通过 semget 来获取),用户无奈让服务器和客户当时认可独特应用哪个描述符,所以有时候就须要到关键字 KEY 来定位描述符。

某个 KEY 只会固定对应一个描述符(这项转换工作由内核实现),这样如果服务器和

客户当时认可独特应用某个 KEY,那么大家就都能定位到同一个描述符,也就能定位到同一个信号量,这样就达到了 SYSTEM V 信号量在过程间共享的目标。

创立和关上信号量


创立一个信号量或拜访一个曾经存在的信号量集。

int semget(key_t key, int nsems, int oflag)

该函数执行胜利返回信号量标示符,失败返回 -1

参数形容 key 通过调用 ftok 函数失去的键值 nsems 代表创立信号量的个数,如果只是拜访而不创立则能够指定该参数为 0,咱们一旦创立了该信号量,就不能更改其信号量个数,只有你不删除该信号量,你就是从新调用该函数创立该键值的信号量,该函数只是返回以前创立的值,不会从新创立;semflg 指定该信号量的读写权限,当创立信号量时不许加 IPC_CREAT,若指定 IPC_CREAT

semget 函数执行胜利后,就产生了一个由内核维持的类型为 semid_ds 构造体的信号量集,返回 semid 就是指向该信号量集的引索。

  • nsems>0 : 创立一个信的信号量集,指定汇合中信号量的数量,一旦创立就不能更改。
  • nsems==0 : 拜访一个已存在的汇合
  • 返回的是一个称为信号量标识符的整数,semop 和 semctl 函数将应用它。
  • 创立胜利后信号量构造被设置:
.sem_perm 的 uid 和 gid 成员被设置成的调用过程的无效用户 ID 和无效组 ID
.oflag 参数中的读写权限位存入 sem_perm.mode
.sem_otime 被置为 0,sem_ctime 被设置为以后工夫
.sem_nsems 被置为 nsems 参数的值 1234

该汇合中的每个信号量不初始化,这些构造是在 semctl,用参数 SET_VAL,SETALL 初始化的。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

#define SEM_R    0400   // 用户(属主)读
#define SEM_A    0200   // 用户(属主)写

#define SVSEM_MODE (SEM_R | SEM_A | SEM_R>>3 | SEM_R>>6)

int main(int argc,char *argv[]) {
    int   c, oflag, semid, nsems;

    oflag = SVSEM_MODE | IPC_CREAT;   // 设置创立模式

    // 依据命令行参数 e 判断是否制订了 IPC_EXCL 模式
    while((c = getopt(argc,argv,"e")) != -1)
    {switch(c)
        {
            case 'e':
                oflag |= IPC_EXCL;
                break;
        }
    }

    // 判断命令行参数是否非法
    if (optind != argc -2)
    {printf("usage: semcreate [-e] <pathname> <nsems>");
        exit(-1);
    }

    // 获取信号量汇合中的信号量个数
    nsems = atoi(argv[optind+1]);

    // 创立信号量,通过 ftok 函数创立一个 key,返回信号量 标识符
    semid = semget(ftok(argv[optind],0),nsems,oflag);


    return EXIT_SUCCESS;
}

关键字的获取


有多种办法使客户机和服务器在同一 IPC 构造上会合:

  • 服务器能够指定关键字 IPC_PRIVATE 创立一个新 IPC 构造,将返回的标识符寄存在某处(例如一个文件)以便客户机取用。关键字 IPC_PRIVATE 保障服务器创立一个新 IPC 构造。这种技术的毛病是:服务器要将整型标识符写到文件中,而后客户机在尔后又要读文件获得此标识符。

IPC_PRIVATE 关键字也可用于父、子关系过程。父过程指定 IPC_PRIVATE 创立一个新 IPC 构造,所返回的标识符在 fork 后可由子过程应用。子过程可将此标识符作为 exec 函数的一个参数传给一个新程序。

  • 在一个专用头文件中定义一个客户机和服务器都认可的关键字。而后服务器指定此关键字创立一个新的 IPC 构造。这种办法的问题是该关键字可能已与一个 IPC 构造相结合,在此状况下,get 函数(msgget、semget 或 shmget)出错返回。服务器必须解决这一谬误,删除已存在的 IPC 构造,而后试着再创立它。当然,这个关键字不能被别的程序所占用。
  • 客户机和服务器认同一个路径名和课题 I D(课题 I D 是 0 ~ 2 5 5 之间的字符值),而后调用函数 ftok 将这两个值变换为一个关键字。这样就防止了应用一个已被占用的关键字的问题。
    应用 ftok 并非居安思危。有这样一种例外:服务器应用 ftok 获取得一个关键字后,该文件就被删除了,而后重建。此时客户端以此重建后的文件来 ftok 所获取的关键字就和服务器的关键字不一样了。所以个别商用的软件都不怎么用 ftok。一般来说,客户机和服务器至多共享一个头文件,所以一个比较简单的办法是防止应用 ftok,而只是在该头文件中寄存一个大家都晓得的关键字。

设置信号量的值(PV 操作)


int semop(int semid, struct sembuf *opsptr, size_t nops);

参数形容 semid 是 semget 返回的 semid 信号量标示符 opsptr 指向信号量操作构造数组 nopsopsptr 所指向的数组中的 sembuf 构造体的个数

该函数执行胜利返回 0,失败返回 -1;

第二个参数 sops 为一个构造体数组指针,构造体定义在 sys/sem.h 中,构造体如下

struct sembuf {
   unsigned short sem_num; /* semaphore index in array */
   short sem_op; /* semaphore operation */
   short sem_flg; /* operation flags */
};

sem_num 操作信号的下标,其值能够为 0 到 nops
sem_flg 为该信号操作的标记:其值能够为 0、IPC_NOWAIT、SEM_UNDO

sem_flg 标识形容 0 在对信号量的操作不能执行的状况下,该操作阻塞到能够执行为止;IPC_NOWAIT 在对信号量的操作不能执行的状况下,该操作立刻返回;SEM_UNDO 当操作的过程推出后,该过程对 sem 进行的操作将被勾销;

sem_op 取值形容 >0>0 则信号量加上它的值,等价于过程开释信号量管制的资源 =0= 0 若没有设置 IPC_NOWAIT,那么调用过程将进入睡眠状态,直到信号量的值为 0,否则过程间接返回 <script type=”math/tex” id=”MathJax-Element-19″>< 0</script> 则信号量加上它的值,等价于过程申请信号量管制的资源,若过程设置 IPC_NOWAIT 则过程再没有可用资源状况下,过程阻塞,否则间接返回。

例如,以后 semval 为 2,而 sem_op = -3,那么怎么办?

留神:semval 是指 semid_ds 中的信号量集中的某个信号量的值

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

int main(int argc,char *argv[]) {
    int     c,i,flag,semid,nops;
    struct  sembuf *ptr;
    flag = 0;
        // 依据命令行参数设置操作模式
    while(( c = getopt(argc,argv,"nu")) != -1)
    {switch(c)
        {
            case 'n':
                flag |= IPC_NOWAIT;   // 非阻塞
                break;
            case 'u':
                flag |= SEM_UNDO;   // 不可复原
                break;
        }
    }
    if(argc - optind < 2)
    {printf("usage: semops [-n] [-u] <pathname> operation...");
        exit(0);
    }
    // 关上一个曾经存在的信号量汇合
    if((semid = semget(ftok(argv[optind],0),0,0)) == -1)
    {perror("semget() error");
        exit(-1);
    }
    optind++;  // 指向以后第一个信号量的地位
    nops = argc - optind;   // 信号量个数
    ptr = calloc(nops,sizeof(struct sembuf));
    for(i=0;i<nops;++i)
    {ptr[i].sem_num = i;  // 信号量变换
        ptr[i].sem_op = atoi(argv[optind+i]);   // 设置信号量的值
        ptr[i].sem_flg = flag;   // 设置操作模式
    }
    // 对信号量执行操作
    if(semop(semid,ptr,nops) == -1)
    {perror("semop() error");
        exit(-1);
    }

    return EXIT_SUCCESS;
}

对信号集履行管制操作(semval 的赋值等)


int semctl(int semid, int semum, int cmd, ../* union semun arg */);

参数形容 semid 是信号量汇合;semnum 是信号在汇合中的序号;semum 是一个必须由用户自定义的构造体,在这里咱们务必弄清楚该构造体的组成:

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

值形容 IPC_STAT 读取一个信号量集的数据结构 semid_ds,并将其存储在 semun 中的 buf 参数中。IPC_SET 设置信号量集的数据结构 semid_ds 中的元素 ipc_perm,其值取自 semun 中的 buf 参数。IPC_RMID 将信号量集从零碎中删除 GETALL 用于读取信号量集中的所有信号量的值,存于 semnu 的 array 中 SETALL 设置所指定的信号量集的每个成员 semval 的值 GETPID 返回最初一个执行 semop 操作的过程的 PID。LSETVAL 把的 val 数据成员设置为以后资源数 GETVAL 把 semval 中的以后值作为函数的返回,即现有的资源数,返回值为非正数。

val 只有 cmd ==SETVAL 时才有用,此时指定的 semval = arg.val。

留神:当 cmd == GETVAL 时,semctl 函数返回的值就是咱们想要的 semval。千万不要认为指定的 semval 被返回到 arg.val 中。

array 指向一个数组,

当 cmd==SETALL 时,就依据 arg.array 来将信号量集的所有值都赋值;

当 cmd ==GETALL 时,就将信号量集的所有值返回到 arg.array 指定的数组中。

buf 指针只在 cmd==IPC_STAT 或 IPC_SET 时有用,作用是 semid 所指向的信号量集

(semid_ds 机构体)。个别状况下不罕用,这里不做议论。

另外,cmd == IPC_RMID 还是比拟有用的。

示例程序


调用 semctl 函数设置信号量的值程序


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

// 定义信号量操作共用体构造
union semun
{
    int                val;
    struct semid_ds    *buf;
    unsigned short     *array;
};

int main(int argc,char *argv[]) {
    int semid,nsems,i;
    struct semid_ds seminfo;
    unsigned short *ptr;
    union semun arg;

    if(argc < 2)
    {printf("usage: semsetvalues <pathname>[values ...]");
            exit(0);
    }

    // 关上曾经存在的信号量汇合
    semid = semget(ftok(argv[1],0),0,0);
    arg.buf = &seminfo;

    // 获取信号量集的相干信息
    semctl(semid,0,IPC_STAT,arg);
    nsems = arg.buf->sem_nsems;  // 信号量的个数
    if(argc != nsems + 2)
    {printf("%s semaphores in set,%d values specified",nsems,argc-2);
        exit(0);
    }

    // 调配信号量
    ptr = calloc(nsems,sizeof(unsigned short));
    arg.array = ptr;

    // 初始化信号量的值
    for(i=0;i<nsems;i++)
    {ptr[i] = atoi(argv[i+2]);
    }


    // 通过 arg 设置信号量汇合
    semctl(semid,0,SETALL,arg);

    return EXIT_SUCCESS;
}

调用 semctl 获取信号量的值


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

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

int main(int argc,char *argv[]) {
    int     semid,nsems,i;
    struct semid_ds seminfo;
    unsigned short *ptr;
    union semun arg;

    if(argc != 2)
    {printf("usage: semgetvalues<pathname>");
        exit(0);
    }

    // 关上曾经存在的信号量
    semid = semget(ftok(argv[1], 0), 0, 0);
    arg.buf = &seminfo;

    // 获取信号量集的属性,返回 semid_ds 构造
    semctl(semid, 0, IPC_STAT, arg);
    nsems = arg.buf->sem_nsems;     // 信号量的数目

    ptr = calloc(nsems,sizeof(unsigned short));
    arg.array = ptr;

    // 获取信号量的值
    semctl(semid, 0, GETALL, arg);
    for(i = 0; i < nsems; i++)
    {printf("semval[%d] = %dn", i, ptr[i]);
    }

    return EXIT_SUCCESS;
}

通过 semctl 实现 PV 操作的函数库

#include <semaphore.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

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

// 将信号量 sem_id 设置为 init_value
int init_sem(int sem_id, int init_value) {
    union semun     sem_union;
    sem_union.val = init_value;

    if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
    {perror("Sem init");

        exit(1);
    }

    return 0;
}

// 删除 sem_id 信号量
int del_sem(int sem_id) {
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    {perror("Sem delete");

        exit(1);
    }
    return 0;
}

// 对 sem_id 执行 p 操作
int sem_p(int sem_id) {
    struct sembuf   sem_buf;
    sem_buf.sem_num = 0;// 信号量编号
    sem_buf.sem_op  = -1;// P 操作
    sem_buf.sem_flg = SEM_UNDO;// 零碎退出前未开释信号量,零碎主动开释

    if (semop(sem_id, &sem_buf, 1) == -1)
    {perror("Sem P operation");

        exit(1);
    }

    return 0;
}

// 对 sem_id 执行 V 操作
int sem_v(int sem_id) {
    struct sembuf   sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op  = 1;// V 操作
    sem_buf.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sem_buf, 1) == -1)
    {perror("Sem V operation");

        exit(1);
    }

    return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
static int nsems;
static int semflg;
static int semid;
int errno=0;
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
int main() {struct sembuf sops[2]; // 要用到两个信号量,所以要定义两个操作数组
int rslt;
unsigned short argarray[80];
arg.array = argarray;
semid = semget(IPC_PRIVATE, 2, 0666);
if(semid < 0)
{printf("semget failed. errno: %dn", errno);
exit(0);
}
// 获取 0th 信号量的原始值
rslt = semctl(semid, 0, GETVAL);
printf("val = %dn",rslt);
// 初始化 0th 信号量,而后再读取,查看初始化有没有胜利
arg.val = 1; // 同一时间只容许一个占有者
semctl(semid, 0, SETVAL, arg);
rslt = semctl(semid, 0, GETVAL);
printf("val = %dn",rslt);
sops[0].sem_num = 0;
sops[0].sem_op = -1;
sops[0].sem_flg = 0;
sops[1].sem_num = 1;
sops[1].sem_op = 1;
sops[1].sem_flg = 0;
rslt=semop(semid, sops, 1); // 申请 0th 信号量,尝试锁定
if (rslt < 0)
{printf("semop failed. errno: %dn", errno);
exit(0);
}
// 能够在这里对资源进行锁定
sops[0].sem_op = 1;
semop(semid, sops, 1); // 开释 0th 信号量
rslt = semctl(semid, 0, GETVAL);
printf("val = %dn",rslt);
rslt=semctl(semid, 0, GETALL, arg);
if (rslt < 0)
{printf("semctl failed. errno: %dn", errno);
exit(0);
}
printf("val1:%d val2: %dn",(unsigned int)argarray[0],(unsigned int)argarray[1]);
if(semctl(semid, 1, IPC_RMID) == -1)
{perror(“semctl failure while clearing reason”);
}
return(0);
}

信号量的牛刀小试——生产者与消费者问题


1.问题形容:
有一个长度为 N 的缓冲池为生产者和消费者所共有,只有缓冲池未满,生产者便可将
音讯送入缓冲池;只有缓冲池未空,消费者便可从缓冲池中取走一个音讯。生产者往缓冲池
放信息的时候,消费者不可操作缓冲池,反之亦然。

2.应用多线程和信号量解决该经典问题的互斥

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

#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define BUFF_SIZE 10

char buffer[BUFF_SIZE];
char count;                         //  缓冲池里的信息数目
sem_t sem_mutex;                    //  生产者和消费者的互斥锁
sem_t p_sem_mutex;                  //  空的时候,对消费者不可进
sem_t c_sem_mutex;                  //  满的时候,对生产者不可进

void * Producer() {while(1)
    {sem_wait(&p_sem_mutex);     //  当缓冲池未满时
        sem_wait(&sem_mutex);       //  期待缓冲池闲暇

        count++;

        sem_post(&sem_mutex);

        if(count < BUFF_SIZE)       //  缓冲池未满
        {sem_post(&p_sem_mutex);
        }

        if(count > 0)               //  缓冲池不为空
        {sem_post(&c_sem_mutex);
        }
    }
}

void * Consumer() {while(1)
    {sem_wait(&c_sem_mutex); //  缓冲池未空时
        sem_wait(&sem_mutex);   //  期待缓冲池闲暇

        count--;
        sem_post(&sem_mutex);

        if(count > 0)
        {sem_post(&c_sem_mutex);
        }
    }

    return NULL;
}

int main() {
    pthread_t ptid,ctid;

    //  initialize the semaphores
    //sem_init(&empty_sem_mutex, 0, 1);
    //sem_init(&full_sem_mutex, 0, `0);

    //creating producer and consumer threads
    if(pthread_create(&ptid, NULL,Producer, NULL))
    {printf("n ERROR creating thread 1");
        exit(1);
    }

    if(pthread_create(&ctid, NULL,Consumer, NULL))
    {printf("n ERROR creating thread 2");
        exit(1);
    }

    if(pthread_join(ptid, NULL)) /* wait for the producer to finish */
    {printf("n ERROR joining thread");
        exit(1);
    }

    if(pthread_join(ctid, NULL)) /* wait for consumer to finish */
    {printf("n ERROR joining thread");
        exit(1);
    }

    //sem_destroy(&empty_sem_mutex);
    //sem_destroy(&full_sem_mutex);
    //exit the main thread
    pthread_exit(NULL);

    return EXIT_SUCCESS;
}
正文完
 0