Linux:进程间通信之信号量

时间:2024-10-04 09:43:42

system V的进程间通信除了共享内存,还有消息队列和信号量

IPC(进程间通信的简称)

消息队列

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法

  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

  • 特性方面

  • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

总之消息队列比较落伍,就是简单提一下

来看一下信号量

信号量

要了解信号量是什么首先我们要了解什么是临界资源

临界资源

能被多个执行流(进程、线程或协程)同时访问的资源就是临界资源,简单理解就是可能会被争抢的资源就是临界资源;例如多进程启动后,同时向显示器打印,显示器就是临界资源。

在进行进程间通信的时候,管道、共享内存、消息队列都是临界资源,那么应该新的问题产生了:临界资源你能用,我能用,但是大家不能一起用(当然了)

所以我们就要进行临界资源的互斥使用,这种进程竞争使用这些资源的关系称为互斥

互斥

互斥:任何时刻只有一个执行流在访问共享资源

在进程中涉及到互斥资源的程序段叫临界区

举个例子,client.c的这部分和server.c的这部分是他们之间的临界区:

client.c:

server.c:

或者多个进程访问显示器的时候,显示器是临界i这样,进程中只有printf区域在访问显示器,printf就叫临界区

深入理解信号量

比如说你要看陶吉吉的演唱会,买到票那么这个座位就是你的;妹买到就不是你的

演唱会的座位票就是临界资源,买票的本质就是对临界资源的预定机制

假如你是米团狗眼的后端程序员,那么你肯定要保证卖出的票不能比座位多吧

所以只能在有票的时候,票才可以被卖,没票的时候,就不能被卖(听上去像废话)

你怎么知道票还有没有?此时就需要引入信号量来维护票的数量,每有一个人买票count--,每有一个人退票count++,但是count本身也是应该临界资源(因为买票和退票判断两端都要看见count是不是>0),count也是临界资源,临界资源怎么保护临界资源?

count多一个就++,少一个就--;如果我们的信号量也是临界资源,那么我们必须保证对count的++和--得是安全的(不会出现负数的情况),怎么保证安全?保证对信号量的操作是原子性的

什么是原子性?

没有中间过程,只有1和0:一件事要么彻底不做,要么就做完

非原子性:做某件事情的时候有中间过程就叫非原子性

我们的count在++和--的时候是不是原子性的呢?

我们的count++和--的时候,要有一个从内存读到CPU的动作,所以本身不是原子性的

还有一个问题,既然我们的信号量可以做++和--,那么他是不是也得被两边看到才能进行对应的++和--?

所以他也得像我之前学的共享内存和管道一样,被两个或多个进程看见

这个信号量他不能做全局变量,因为全局变量不能被多个进程看见

所以一个优秀的信号量应该保证自己的安全(原子性)+可以被别的进程看见

我们的信号量就可以解决生产者-消费者、读-写这种的互斥问题

什么是同步?

这里的同步并不是传统意义的同步。例如父进程获得的数据需要写到管道里,子进程拿走数据做计算并输出是一个正常的顺序的话,那么按照这个顺序的才是同步;也就是说进程之间按照彼此的依赖关系运行才叫同步

实现信号量的操作

信号量有两种状态

当count--的时候叫P(等待):如果信号量的值大于0,就可以进行正常访问资源,并且进行--;如果等于0,就要进行阻塞

当count++的时候叫V(释放):如果信号量绑定的进程或线程完成了一次对共享资源的访问,信号量++

申请信号量的接口:

semget()

  • key:用于标识信号量集的键值。通常可以使用 ftok 函数生成。

  • nsems:指定将创建或访问的信号量集中的信号量数量。

  • semflg:标志参数,用于指定信号量集的创建和访问方式。

fotk()

设置获取信号量的状态、设置信号量的值、删除信号量:

semctl()

  • semid:目标信号量集的标识符。

  • semnum:用于指定要操作的具体信号量的索引。对于整个信号量集的操作,此参数一般设置为 0。

  • cmd:指定要执行的控制操作。

  • semctl() 函数的第四个参数为可选参数,根据不同的 cmd 命令,它的类型和用法可能不同。

  • 常用的 cmd 控制命令如下:

  • IPC_STAT:获取信号量集的状态信息,将结果存储在 semun 结构体中。

  • SETVAL:设置单个信号量的值。第四个参数为一个新的值,可以是整数或结构体 semun

  • GETVAL:获取单个信号量的值。

  • IPC_RMID:删除整个信号量集。

插播一下,我抢到陶吉吉的演唱会票了

sem.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>        
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sem.h>
#define MY_FIFO "./fifo"
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0X6667
#define SIZE 4097

union semun{
    int val;
    struct semid_ds *buf;
};//联合体,联合体内的成员共享同一块空间

int main(){
    //创建key
    int key=ftok(PATH_NAME,PROJ_ID);
    if(key==-1){
        perror("ftok");
        return 1;
    }
    //创建信号量
    int semid= semget(key,3,IPC_EXCL|IPC_CREAT);
    if(semid<0){
        perror("semget");
        return -1;
     }
    //设置信号量的值
    union semun argv;
    argv.val=1;
    int result=semctl(semid,0,SETVAL,argv);
    if(result<0){
        perror("semctl");
        return -1;
    }
    //获取信号量的值
    int val=semctl(semid,0,GETVAL);
    if(val<0){
        perror("semctl val");
        return -1;
    }
    printf("val==%d\n",val);
    //删除信号量
     result = semctl(semid, 0, IPC_RMID);
    if (result == -1) {
        perror("semctl del");
        return -1;
    }

    printf("key==%d\nsemid==%d\n",key,semid);
   
    
    return 0;
}

一开始写成这样发现提示:semctl: Permission denied

于是给semctl加上权限0666:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>        
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sem.h>
#define MY_FIFO "./fifo"
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0X6667
#define SIZE 4097

union semun{
    int val;
    struct semid_ds *buf;
};//联合体,联合体内的成员共享同一块空间

int main(){
    //创建key
    int key=ftok(PATH_NAME,PROJ_ID);
    if(key==-1){
        perror("ftok");
        return 1;
    }
    //创建信号量
    int semid= semget(key,3,IPC_EXCL|IPC_CREAT|0666);
    if(semid<0){
        perror("semget");
        return -1;
     }
    //设置信号量的值
    union semun argv;
    argv.val=1;
    int result=semctl(semid,0,SETVAL,argv);
    if(result<0){
        perror("semctl");
        return -1;
    }
    //获取信号量的值
    int val=semctl(semid,0,GETVAL);
    if(val<0){
        perror("semctl val");
        return -1;
    }
    printf("val==%d\n",val);
    //删除信号量
     result = semctl(semid, 0, IPC_RMID);
    if (result == -1) {
        perror("semctl del");
        return -1;
    }

    printf("key==%d\nsemid==%d\n",key,semid);
   
    
    return 0;
}

这样就好啦

我们也可以用命令查看我们的信号状态:

ipcs -s

有信号量的时候是这样:

删除是:

ipcrm -s semid

 对信号集的进行等待和释放操作(实现PV操作):

semop()

  • semid:目标信号量集的标识符。

  • sops:指向 sembuf 结构体数组的指针,每个结构体表示一个信号量操作。

  • nsops:指定要执行的信号量操作的数量。

sem.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>        
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sem.h>
#define MY_FIFO "./fifo"
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0X6667
#define SIZE 4097

union semun{
    int val;
    struct semid_ds *buf;
};//联合体,联合体内的成员共享同一块空间
struct sembuf{
    unsigned short sem_num;//信号量的索引
    short sem_op;//信号量操作,<0则表示P,>0则表示V,0表示等待
    short sem_flg;//操作标志,IPC_NOWAIT(非阻塞)和SEM_UNDO(系统恢复时撤销操作)
};



int main(){
    //创建key
    int key=ftok(PATH_NAME,PROJ_ID);
    if(key==-1){
        perror("ftok");
        return 1;
    }
    //创建信号量
    int semid= semget(key,3,IPC_EXCL|IPC_CREAT|0666);
    if(semid<0){
        perror("semget");
        return -1;
     }
    //设置信号量的值
    union semun argv;
    argv.val=1;
    int result=semctl(semid,0,SETVAL,argv);
    if(result<0){
        perror("semctl");
        return -1;
    }
    //等待信号量的值为0
    struct sembuf sop;
    sop.sem_num=0;
    sop.sem_op=0;
    sop.sem_flg=0;
    result=semop(semid,&sop,1);
    if(result<0){
        perror("semop");
        return -1;
    }
    printf("Semaphore operation complete.\n");
    //对信号量进行P操作
    sop.sem_op=-1;
    result=semop(semid,&sop,1);
      if(result<0){
        perror("semop");
        return -1;
    }
    //进行V操作
    sop.sem_op=1;
    result=sempo(semid,&sop,1);
      if(result<0){
        perror("semop");
        return -1;
    }
    //删除信号量
     result = semctl(semid, 0, IPC_RMID);
    if (result == -1) {
        perror("semctl del");
        return -1;
    }
    printf("key==%d\nsemid==%d\n",key,semid); 
    return 0;
}

我们现在学了这么多通信方式,那么操作系统是怎么管理这些文件的?

通过结构体:

struct ipc_id_ary
{
    int size;
    struct kern_ipc_pern* p[0];
    ...
}

我们之前学了shmid就是数组的下标,那么把它们的第一个元素都存到struct kern_ipc_perm *XXX[n],就意味着我们把IPC的资源统一管理了

来写一个利用信号量的完整流程:

sem.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>        
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sem.h>
#define MY_FIFO "./fifo"
#include<sys/shm.h>
#include <errno.h>
#define PATH_NAME "./"
#define PROJ_ID 0X6667

union semun{
    int val;
    struct semid_ds *buf;
};//联合体,联合体内的成员共享同一块空间
/*struct sembuf{
    unsigned short sem_num;//信号量的索引
    short sem_op;//信号量操作,<0则表示P,>0则表示V,0表示等待
    short sem_flg;//操作标志,IPC_NOWAIT(非阻塞)和SEM_UNDO(系统恢复时撤销操作)
};
*/
void mysemop(int num,int op,int semid){
    struct sembuf buf;
    buf.sem_num=num;
    buf.sem_op=op;
    buf.sem_flg=0;
    semop(semid,&buf,1);
}

int main(){
    //创建key
    int key=ftok(PATH_NAME,PROJ_ID);
    if(key==-1){
        perror("ftok");
        return 1;
    }
    //创建信号量
    int semid= semget(key,2,IPC_EXCL|IPC_CREAT|0666);
    if(semid<0){
        if(errno==17){
            semget(key,2,0666);
        }else{
             perror("semget");
             exit(0);
            }
     }
    printf("key==%d\nsemid==%d\n",key,semid);
    //设置信号量的值
    union semun sem;
    sem.val=0;
    int result=semctl(semid,0,SETVAL,sem);
    if(result<0){
        perror("semctl");
        return -1;
    }
    sem.val=10;
    result=semctl(semid,1,SETVAL,sem);
    if(result<0){
        perror("semctl");
        return -1;
    }
    //对信号量进行PV操作
    int i=6;
    while(i--){
        mysemop(1,-1,semid);
        printf("sem==%d\n",semctl(semid,1,GETVAL));//获取信号量的值
    }
    //删除信号量
    result = semctl(semid, 0, IPC_RMID);
    if (result <0) {
        perror("semctl del");
        return -1;
    }
 
    return 0;
}