学科分类
目录
Linux编程

信号量

Linux系统采用多道程序设计技术,允许多个进程同时在内核中运行,但同一个系统中的多个进程之间,可能因为进程合作或资源共享,产生制约关系。制约关系分为直接相互制约关系和间接相互制约关系:

(1)直接相互制约关系。利用管道机制实现进程间通信,当管道为空时,读进程由于无法从管道中读取数据而进入阻塞;当管道存满时,写进程由于无法向管道中写入数据而进入阻塞,类似于这种需要进程间协调合作导致的制约关系,称为直接相互制约关系。

(2)间接相互制约关系。若当前系统中只有1台打印机,当进程A占用打印机时,进程B也申请使用打印机,进程B就会进入阻塞,等待打印机释放;同样若B先获取到打印机,A进程申请使用打印机后也会进入阻塞。类似于这种因资源共享导致的制约关系,称为间接相互制约关系。

直接相互制约的进程间有同步关系,间接相互制约的进程间有互斥关系,同步与互斥存在的根源是系统中存在临界资源(Critical Resouce)。计算机中的硬件资源(如内存、打印机、磁盘)以及软件资源(如共享代码段、变量等)都是临界资源,为了避免多进程的并发执行造成的不一致性,临界资源在同一时刻只允许有限个进程对其进行访问或修改。

计算机中的多个进程必须互斥地访问系统中的临界资源,用于访问临界资源的代码称为临界区(Critical Section),临界区也属于临界资源,若能保证进程间互斥地进入自己的临界区,就能实现进程对临界资源的互斥访问。

信号量(Semaphore)是专门用于解决进程同步与互斥问题的一种通信机制,它与信号无关,也不同于管道、FIFO以及消息队列,一般不用来传输数据,信号量包括一个被称为信号量的表示资源数量的非负整型变量、修改信号量的原子操作P和V,以及该信号量下等待资源的进程队列。

在Linux系统中,不同的进程通过获取同一个信号量键值进行通信,实现进程间对资源的互斥访问。使用信号量进行通信时,通常需要以下步骤:

(1)创建信号量/信号量集,或获取系统中已有的信号量/信号量集;

(2)初始化信号量。早期信号量通常被初始为1,但有些进程一次需要多个同类的临界资源,或多个不同类且不唯一的临界资源,因此可能需要初始化的不是信号量,而是一个信号量集;

(3)信号量的P、V操作,根据进程请求,修改信号量的数量。执行P操作会使信号量-1,执行V操作会使信号量+1;

(4)从系统中删除不需要的信号量。

系统中信号量的数量是有限制的,其极限值由宏SEMMSL设定。Linux内核提供了三个系统调用,用于实现以上步骤,这三个系统调用接口分别为:semget()、semctl()和semop(),下面分别对这三个系统调用进行讲解。

① semget()

semget()函数的功能为创建一个新的信号集,或获取一个系统中已经存在的信号量集,该函数存在于函数库sys/sem.h中,其函数声明如下:

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

若该函数调用成功则返回信号量的标识符,否则返回-1,并设置errno,常见errno的值与其含义如下:

● EACCES。表示进程无访问权限;

● ENOENT。表示传入的键值不存在;

● EINVAL。表示nsens小于0,或信号量数已达上限;

● EEXIST。当semflg设置指定了ICP_CREAT和IPC_EXCL时,表示该信号量已经存在。

semget()函数中的参数key表示信号量的键值,通常为一个整数;参数nsems表示创建的信号量数目;参数semflg为标志位,与open()、msgget()函数中的标志位功能相似,都用来设置权限,权限位可与IPC_CREAT以及IPC_EXCL发生位或,另外若该标志位设置为IPC_PRIVATE,表示该信号量为当前进程的私有信号量。

② semctl()

semctl()函数可以对信号量或信号量集进行多种控制,该函数存在于函数库sys/sem.h中,其函数声明如下:

int semctl(int semid, int semnum, int cmd, ...);

若该函数调用成功则根据参数cmd的取值返回相应信息,通常为一个非负整数;否则返回-1并设置errno。

semctl()函数的参数semid表示信号量标识符,通常为semget()的返回值;参数semnum表示信号量在信号量集中的编号,该参数在使用信号量集时才会使用,通常设置为0,表示取第一个信号;参数cmd表示对信号量进行的操作;最后一个参数是一个可选参数,依赖于参数cmd,使用该参数时,用户必须在程序中自定义一个如下所示的共用体:

union semun{
    int val;                        //cmd为SETVAL时,用于指定信号量值
    struct semid_ds *buf;            //cmd为IPC_STAT时或IPC_SET时生效
    unsigned short *array;            //cmd为GETALL或SETALL时生效
    struct seminfo *_buf;            //cmd为IPC_INFO时生效
};

在该共用体中的struct semid_ds是一个由内核维护,记录信号量属性信息的结构体,该结构体的类型定义如下:

struct semid_ds {
    struct ipc_perm sem_perm;        //所有者和标识权限
    time_t            sem_otime;        //最后操作时间
    time_t            sem_ctime;        //最后更改时间
    unsigned short  sem_nsems;        //信号集中的信号数量
};

cmd常用的设置为SETVAL和IPC_RMID,其含义分别如下:

● SETVAL。表示semctl()的功能为初始化信号量的值,信号量值通过可选参数传入,在使用信号量前应先对信号量值进行设置;

● IPC_RMID。表示semctl()的功能为从系统中删除指定信号量。信号量的删除应由其所有者或创建者进行,没有被删除的信号量将会一直存在于系统中。

① semop()

semop()函数的功能为改变信号量的值,该函数存在于函数库sys/sem.h中,函数声明如下:

int semop(int semid, struct sembuf *sops, unsigned nsops);

若该函数调用成功返回0,否则返回-1,并设置errno。

semop()函数的参数semid同样为semget()返回的信号量标识符;参数nsops表示参数sops所指数组中元素的个数。

参数sops为一个struct sembuf类型的数组指针,该数组中的每个元素设置了要对信号量集中的哪个信号做哪种操作,struct sembuf结构体定义如下:

struct sembuf{
  short sem_num;     //信号量在信号量集中的编号
  short sem_op;     //信号量操
  short sem_flag;    //标志位
};

当结构体成员sem_op设置为-1时,表示P操作;设置为+1时,表示V操作。结构体成员sem_flg通常设置为SEM_UNDO,若进程退出前没有删除信号量,信号量将会由系统自动释放。

下面通过案例来展示信号量相关的系统调用接口的使用方法。

案例5:使用信号量实现父子进程同步,防止父子进程抢夺cpu。案例实现如下:

 1    #include <stdio.h>
 2    #include <stdlib.h>
 3    #include <sys/sem.h>
 4    //自定义共用体
 5    union semu{
 6        int val;
 7        struct semid_ds* buf;
 8        unsigned short* array;
 9        struct seminfo* _buf;
 10    };
 11    static int sem_id;
 12    //设置信号量值
 13    static int set_semvalue()
 14    {
 15        union semu sem_union;
 16        sem_union.val=1;
 17        if(semctl(sem_id,0,SETVAL,sem_union)==-1)
 18            return 0;
 19        return 1;
 20    }
 21    //p操作,获取信号量
 22    static int semaphore_p()
 23    {
 24        struct sembuf sem_b;
 25        sem_b.sem_num = 0;
 26        sem_b.sem_op = -1;
 27        sem_b.sem_flg = SEM_UNDO;
 28        if(semop(sem_id,&sem_b,1)==-1){
 29            perror("sem_p err");
 30            return 0;
 31        }
 32        return 1;
 33    }
 34    //V操作,释放信号量
 35    static int semaphore_v()
 36    {
 37        struct sembuf sem_b;
 38        sem_b.sem_num=0;
 39        sem_b.sem_op=1;
 40        sem_b.sem_flg=SEM_UNDO;
 41        if(semop(sem_id,&sem_b,1)==-1){
 42            perror("sem_v err");
 43            return 0;
 44        }
 45        return 1;
 46    }
 47    //删除信号量
 48    static void del_semvalue()
 49    {
 50        union semu sem_union;
 51        if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
 52            perror("del err");
 53    }
 54    int main()
 55    {
 56        int i;
 57        pid_t pid;
 58        char ch='C';
 59        sem_id=semget((key_t)1000,1,0664|IPC_CREAT);//创建信号量
 60        if(sem_id==-1){
 61            perror("sem_c err");
 62            exit(-1);
 63        }
 64        if(!set_semvalue()){                    //设置信号量值
 65            perror("init err");
 66            exit(-1);
 67        }
 68        pid=fork();                                //创建子进程
 69        if(pid==-1){                                //若创建失败
 70            del_semvalue();                        //删除信号量
 71            exit(-1);
 72        }
 73        else if(pid==0)                             //设置子进程打印的字符
 74            ch='Z';
 75        else                                        //设置父进程打印的字符
 76            ch='C';
 77        srand((unsigned int)getpid());            //设置随机数种子
 78        for(i=0;i<8;i++)                            //循环打印字符
 79        {                        
 80            semaphore_p();                        //获取信号量
 81            printf("%c",ch);                        
 82            fflush(stdout);                        //将字符打印到屏幕
 83            sleep(rand()%4);                        //沉睡
 84            printf("%c",ch);                        
 85            fflush(stdout);                        //再次打印到屏幕
 86            sleep(1);
 87            semaphore_v();                        //释放信号量
 88        }
 89        if(pid>0){
 90            wait(NULL);                            //回收子进程
 91            del_semvalue();                        //删除信号量
 92        }
 93        printf("\nprocess %d finished.\n",getpid());            
 94        return 0;
 95    }

编译案例,执行程序,程序的运行结果如下:

CCZZCCZZCCZZCCZZCCZZCCZZCCZZCCZZ
process 3657 finished 

process 3656 finished

观察运行结果,字符C与字符Z总是成对出现,这是因为案例5主函数的for()循环中进行了两次打印操作,且程序使用了一个二值信号量,将这两次打印操作绑定为了一个原子操作:代码第80行调用了semaphore_p()函数获取信号量,若获取信号量的是父进程,那么子进程将无法获取cpu,除非父进程调用semaphore_v()函数将信号量释放,否则子进程无法执行for循环中的核心代码;反之子进程获取信号量之后,父进程也无法获取cpu。

结合程序运行结果,根据以上分析可知,案例5实现成功。

多学一招:ftok()函数

当在进程中使用System V IPC系列的接口进行通信时,必须指定一个key值,这是一个key_t类型的变量,通常不会直接使用具体数值,而是通过Linux系统中的一个函数——ftok()来获取。ftok()函数位于函数库sys/types中,其定义如下:

key_t ftok(const char *pathname, int proj_id);

该函数的参数pathname表示路径名,一般会设置为当前目录“.”;参数proj_id由用户指定,为一个整型数据,一般设置为0。当ftok()函数被调用时,该函数首先会获取目录的inode,其次将十进制的inode及参数proj_id否转换为十六进制,最后将这两个十六进制数链接,生成一个key_t类型的返回值。

例如,当前目录的inode值为65538,转换为十六进制为0x01002;指定的proj_id值为24,转换为十六进制为0x18,那么ftok()返回的key值则为0x18010002。

点击此处
隐藏目录