Posix信号灯和SystemV信号灯解析


暑假那会把UNPv2大概过了一遍,不过没有总结,除了pipe,所以现在都忘得差不多了,趁最近几天学操作系统,边学边回顾一下。


信号灯(semaphore)是一种用于提供不同进程间或一个给定进程的不同线程的同步手段的原语。

我所知一共有三种类型信号灯:

  • Posix有名信号灯:使用Posix IPC名字标识,可用于进程或现程间的同步
  • Posix基于内存的信号灯:存放在共享内存区,可用于进程或线程间的同步
  • System V信号灯:在内核中维护,可用于进程或线程间的同步
Posix 有名信号灯,有名信号灯可能与文件系统中路径的名字来标识的。我们在使用Posix有名信号灯是,可以使用 ll /dev/shm查看是否成功打开了信号灯。
Posix信号灯和SystemV信号灯解析_第1张图片
Posix 基于内存的信号灯,基于共享内存的信号灯和有名信号灯虽然实现机制不一样,但在编程中仅有创建和销毁两个函数不一样。
Posix信号灯和SystemV信号灯解析_第2张图片
System V信号灯:
Posix信号灯和SystemV信号灯解析_第3张图片

此外,如果按值来区分,信号灯有 普通信号灯二值信号灯之分。后者大致相当于mutex,不过后者可被不同进程或线程解锁,而mutex只能被持有锁的线程解锁。


下面给出信号灯、互斥锁、条件变量之间的三个差异:

  1. 互斥锁必须总是由给它上锁的线程解锁,信号灯则可以由其它线程解锁。
  2. 互斥锁要么被锁住,要么被解开(二值状态,类似于二值信号灯)
  3. 既然信号灯有一个与之关联的状态(它的计数值),信号灯挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失(丢失就是好像没出现过这货一样)。

一:Posix有名信号灯


Posix信号灯和SystemV信号灯解析_第4张图片

Posix有名信号灯编程函数:

sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
                     mode_t mode, unsigned int value);
name:打开信号灯,name是给信号灯起的名字,也就是信号灯对应在文件系统中的路径,加入你打开了一个信号灯,该信号灯是随内核持续的,就算你的程序结束了,信号灯             依然存在,ll /dev/shm即可看到。销毁信号灯需要程序中调用unlink函数(对于使用内存的信号灯而言是destroy函数)或者机器重启,调用close函数也不行。
oflag:打开方式,常用O_CREAT | O_EXCL,IPC这几个家伙几乎都用这两个
mode:文件权限。常用#define FILE_MODE   (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH),本用户可读可写,同组用户可读,其他用户读。
value:信号量值。二元信号灯值为1,普通的话,就用来表示资源数目就行。

注意的是:出错返回值的定义 #define SEM_FAILED ((sem_t*)(-1))

int sem_close(sem_t *sem);
这个函数虽说close,实际没什么乱用,即便不使用该函数,eixt或_exit的时候也会自动关闭信号灯。但是不能从系统中删除。失败返回-1。

int sem_unlink(const char* name);
name:信号灯对应的文件系统名字。
该函数作用是将信号灯从文件系统删除。

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
让信号灯减1,信号灯值已经是0的话,调用线程将阻塞。try_wait函数则不会阻塞,会返回EAGAIN错误。如果某个信号中断,sem_wait就可能过早地返回,所返回的错误为EINTR。

int sem_post(sem_t *sem);
与上面函数作用相反,加1函数。

int sem_getvalue(sem_t* sem, int *valp);
使用Int*获得当前信号灯的值。


下面是使用有名信号灯写的单生产者单消费者模型:
#include "../unp.h"

#define NBUFF 10
#define SEM_MUTEX "mutex"   //thers are args to ipc name.
#define SEM_NEMPTY "nempty"
#define SEM_NSTORED "nstored"

int nitems;   //read-only by producer and consumer
struct {
    int    buff_[NBUFF];
    sem_t* mutex_;
    sem_t* nempty_;
    sem_t* nstored_;
} shared;

void *produce(void *), *consume(void *); 

int main(int argc, char** argv)
{
    if(argc != 2)
        err_quit("usage: prodcons1 <#items>");
    nitems = atoi(argv[1]);

    //create three semephore
    shared.mutex_ = sem_open(SEM_MUTEX, O_CREAT | O_EXCL, FILE_MODE, 1);  //1 
    assert(shared.mutex_ != (sem_t*)(-1));
    shared.nempty_ = sem_open(SEM_NEMPTY, O_CREAT | O_EXCL, FILE_MODE, NBUFF);  //NBUFF
    assert(shared.nempty_ != (sem_t*)(-1));
    shared.nstored_ = sem_open(SEM_NSTORED, O_CREAT | O_EXCL, FILE_MODE, 0);  //0 
    assert(shared.nstored_ != (sem_t*)(-1));

    //create one producer thread and one consumer thread
    int ret = 0;
    pthread_t tid_produce, tid_consume;
    ret = pthread_create(&tid_produce, NULL, produce, NULL);
    assert(ret != -1);
    ret = pthread_create(&tid_consume, NULL, consume, NULL);
    assert(ret != -1);
    
    //wait for the two threads
    pthread_join(tid_produce, NULL);
    pthread_join(tid_consume, NULL);
    
    sem_unlink(SEM_MUTEX);
    sem_unlink(SEM_NEMPTY);
    sem_unlink(SEM_NSTORED);
    exit(0);
}   

void *produce(void *)
{
    for(int i=0; i


二:Posix基于内存的信号灯



基于内存的信号灯和有名信号灯变成基本都是一样的。有名信号灯天生支持不同进程间共享,基于内存的信号的通过是指SHARED来确定是否除了本进程内部线程共享其他进程也要共享。

它们两种只有两个函数有区别:
int sem_init(sem_t *sem, int shared, unsigned int value);
初始化信号灯。sem参数必须指向有应用程序分配的sem_t变量。如果shared为0,那么待初始化的信号灯是在同一进程中的各个线程间共享的,否则该信号灯是在进程间共享的。当shared为非零时,该信号灯必须存放在即将使用它的所有进程都能访问的某种类型的共享内存中。value是初始值。

int sem_destroy(sem_t* sem);
使用完一个基于内存的信号灯后,我们调用sem_destroy摧毁它。

代码示例就不写了,和上面只差两个函数。

Posix定义了两个信号灯的限制,可通过sysconf函数查看:
  1. SEM_NSEMS_MAX    一个进程可同时打开着的最大信号灯数(Posix要求至少为256)
  2. SEM_VALUE_MAX     一个信号灯的最大值(Posix要求至少为32767)


三:System V信号灯



System V信号灯通过定义如下概念给信号灯增加了另外一级复杂度:
  • 计数信号灯集(set of counting semaphore):一个或多个信号灯(构成一个集合),其中每个都是计算信号灯。每个集合的信号灯数存在一个限制,一般在25个数量级上。当我们谈论“System V信号灯”时,所指的是计数信号灯集。当我们谈论“Posix信号灯”时,所指的是单个计数信号灯。

对于系统中的每个信号量集,内核维护一个如下的信息结构:
struct semid_ds {
    struct ipc_permsem_perm ;
    structsem*    sem_base ; //信号数组指针
    ushort        sem_nsem ; //此集中信号个数
    time_t        sem_otime ; //最后一次semop时间
    time_t        sem_ctime ; //最后一次创建时间
} ;

某个给定信号量的结构体:
struct sem {
    ushort_t  semval ;  //信号量的值
    short     sempid ;  //最后一个调用semop的进程ID
    ushort    semncnt ; //等待该信号量值大于当前值的进程数(一有进程释放资源 就被唤醒)
    ushort    semzcnt ; //等待该信号量值等于0的进程数
} ; 


System V信号量常用函数:
key_t  ftok(char *pathname,  char proj); 

根据参数pathnameproj来创建一个关键字,成功时返回与路径pathname相对应的一个键值,具有唯一性,失败时返回值为-1.

int  semget (key_t  key,  int  nsems ,  int  semflg);

创建一个新信号量或者取得一个现有的信号量,key是一个关键字,可以是用ftok()函数创建的,也可以是IPC_PRIVATE/usr/include/bits$ vi ipc.h),nsems表明创建的信号量个数,semflg是设置信号量的访问权限标志,函数调用成功时返回信号量ID,失败则返回-1.

int semop (int semid, struct sembuf *spos, int nspos);

对信号量进行操作的函数,用于改变信号量的键值,semid是信号量的标志,spos是指向一个结构体数组的指针,表明要进行什么操作,nspos表明数组的元素个数,调用成功则返回0,失败则返回-1。/usr/include/linux$ vi sem.h  (sembuf \ semun)

 Struct sembuf  
 {
    Unsigned short sem_num; /*sem index in array*/
    Short sem_op; /* sem operation */
    Short sem_flg;/* operation flags */ sem_flg&IPC_RND 0
};

其中,如果sem_op大于0,那么操作值加入到信号量的值中,并唤醒等待信号增加的进程,如果sem_op为0,当信号量的值是0的时候,函数返回,否则阻塞直到信号量的值为0,如果sem_op小于0,函数判断信号量的值加上这个负值,如果结果为0唤醒等待信号量为0的进程,如果小于0函数阻塞,如果大于0,那么从信号量里面减去这个值并返回。

int semctl (int semid, int semnum, int cmd,  union semun arg);

该函数得作用是对信号量进行一系列得控制,semid是要操作得信号量标志,semnum是信号量的下标,cmd是操作的命令,经常用的两个命令是:SETVALIPC_RMIDarg用于设置或返回信号量信息。

 Union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
    void *__pad;
}



















你可能感兴趣的:(操作系统)