三、 Linux——进程间通信

三、 Linux——进程间通信

3.1 进程间通信(IPC)概述

进程间通信是指在不同进程之间传播或交换信息,IPC的方式常有管道(包括无名管道和命名管道)消息队列,信号量,共享存储,Socket,Stream等。其中Socket和Stream支持不同主机上的两个进程IPC。

3.2 无名管道(PIPE)

PIPE(无名管道):

管道,通常指 无名管道 ,是UNIX系统IPC最古老的形式。只存在于内存中,当父子进程退出后,管道就消失了。

特点

  • 它是 半双工 的(即数据只能在一个方向上流动),具有固定的读端和写端。
  • 它只是用于具有 亲缘关系 的进程之间 通信(也是父子进程或者兄弟进程之间【两个由同一个父进程创建的子进程】)
  • 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read,write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且 只存在于内存中 。当父子进程退出后,管道就消失了。

在这里插入图片描述

原型

#include<unistd.h>
int pipe(int fd[2]);    //返回值 :若成功返回0   失败返回-1

返回值:

若成功返回0 失败返回-1

当一个管道建立时候,它会创建两个文件描述符: fd[0] 为读而打开 ,fd[1] 为写而打开。

关闭管道,直接用close就可以

Demo: 父进程往管道里写,子进程往管道里读

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


int main()
{
     int fd [2];   //两个文件描述符
     int  pid;
     char buff [128];
      if(pipe(fd) == -1){  //创建管道
            printf("Create pipe error! \n"); //创建管道错误  
      }
       pid = fork();    //创建子进程
       if(pid <0 ){
             printf("Fork  error  \n");   //fork错误
       }else  if(pid > 0){   //父进程
                sleep(3);
                printf("this is father\n");
                close(fd[0]);    //关闭读端
                 write(fd[1],"hello from father\n",strlen("hello from father"));
                wait();
       }else{
                 printf("this is child\n");
                 close(fd[1]);    //关闭写端
                 read(fd[0],buff,128);
                 printf("read from father:%s\n",buff);
                 exit(0);
        }
         return 0;
}


父进程等待3秒写入的时候,发现read 没读到数据 ,read会阻塞,等到父进程写入后,子进程执行read读出数据

3.3 命名管道(FIFO)

FIFO,也称为命名管道,它是一种文件类型。
命名管道是一种特殊类型的文件,它以文件的形式存在,但不是真正的文件,并且不保留数据,只是起到一个数据传输的作用。

  1. 特点

    1. FIFO可以在无关进程之间交换数据,与无名管道不同。
    2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
  2. 原型

    #include<sys/stat.h>
    #include<sys/types.h>
    //返回值:成功返回0 ,失败返回-1
    int mkfifo(char *pathname , mode_t mode);
    

    参数说明

    pathname:文件路径
    mode:文件权限

    mode与open函数中的mode相同。一旦创建一个FIFO,就可以用一般的文件I/O函数操作它(如 open ,read ,write)。

1、打开命名管道文件FIFO

1、FIFO和通过pipe调用创建的管道不同,它是一个有名字的文件而表示一个打开的文件描述符。

2、在对它进行读或写操作之前必须先打开它。

3、FIFO文件也要用open和close函数来打开或关闭,除了一些额外的功能外,整个操作过程与文件操作是一样的。

4、传递给open调用的是一个FIFO文件的路径名,而不是一个正常文件的路径名。
2、创建命名管道DEMO

#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<errno.h>

// int mkfifo(const char *pathname ,mode_t mode);

int main(){
      if((mkfifo("./file",0600)== -1)&& errno!=EEXIST){
              printf("mkfifo failuer\n");
              perror("why");
      }
      return 0;
}

运行结果

在这里插入图片描述

这时直接打开管道是打不开的,命名管道有自己的打开规则

3、命名管道的打开规则

1、**如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;**否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志,即只设置了O_RDONLY),反之,如果当前打开操作没有设置了非阻塞标志,即O_NONBLOCK,则返回成功

2、如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

3、通俗的说,要打开一个FIFO命名管道文件,需要一个进程以写打开,并且另一个进程要以读打开,只有满足了有读打开和有写打开,命名管道才算打开成功。就像水管一样,水管的进口和出口需要同时打开,水才能流过去。

4、当open一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别

  • 若没有指定O_NONBLOCK(默认),只读open要阻塞到某个其他进程为写而打开此FIFO ,类似的,只写open要阻塞到某个其他他进程为读而打开它。

  • 若指定了O_NOBLOCK,则只读open立即返回。而只写open将出错误返回-1 如果没有进程已经为读而打开该FIFO,其errno置ENXIO 。

Demo 两个进程之间循环通信

#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>

int main(){

      int buf[30]={0};
      int nread=0;

      int fd = open("./file",O_RDONLY);
      printf("read open success\n");

      while(1){
         nread = read(fd,buf,30);
         printf("read%d byte from fifo context:%s\n",nread,buf); 
         } 
      return 0;
}
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>

int main(){

      int cnt=0;
      char *str="message from fifo";

      int fd = open("./file",O_WRONLY);
      printf("write open success\n");
      while(1){
      write(fd,str,strlen(str));
      sleep(1);
        if(cnt == 5){
             break;
        }
      }
      return 0;
}

运行结果

在这里插入图片描述

5、管道总结

所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

我们可以得知,对于无名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。

另外,对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。

不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

3.4 消息队列

1、消息队列的通信原理

消息队列可以实现互发,消息队列是消息的连接表,存放在内核中,一个消息队列由一个标识符(即队列ID)来标识。

特点

  1. 消息队列是面向记录的。其中的消息具有特定的格式以及特定优先级
  2. 消息队列独立于发生与接收进程,进程终止时,消息队列及内容并不会被删除。
  3. 消息队列可以实现消息的随机查询消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

函数原型

#include<sys/msg.h>
//创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key ,int flag);
//添加消息:成功返回0,失败返回-1
int msgsnd(int msqid , void *ptr , size_t size ,int flag);
//读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv (int masqid ,void *ptr ,size ,long type ,int flag);
//控制消息队列:成功返回0 ,失败返回-1
int msgctl (int msqid ,int cmd ,struct msqid_ds *buf);

在这里插入图片描述

2、相关API

创建消息队列

//创建或打开消息队列:成功时返回队列ID 失败返回-1
int msgget(key_t key ,int flag);

key:非负数,索引值 从内核中找到队列

flag:打开队列的方式

以下两种情况,函数将创建一个新的消息队列:

  • 没有与key值对应的消息队列,且flag中包含了IPPC_CREAT标志位
  • key参数为IPC PRIVATE

如何添加消息队列的消息

//添加消息:成功返回0,失败返回-1
int msgsnd(int msqid , void *ptr , size_t size ,int flag);

msqid :队列ID
ptr: 消息内容
size :消息大小
flag: 打开队列方式

如何在消息队列中取得信息

//读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv (int masqid ,void *ptr ,size ,long type ,int flag);

msqid :队列ID
ptr: 消息
size :消息大小
type: 队列类型

type == 0 ,返回队列中的第一个消息;
type > 0 返回队列中消息类型为type 的第一个消息;
type < 0 返回队列中消息类型值小于或者等于type 绝对值的消息,如果由多个,则取类型值最小的消息
type值非0 时用于以非先进先出次序读消息。也可以把type看做优先级的权值

flag: 打开队列方式

控制消息队列

//控制消息队列:成功返回0 ,失败返回-1
int msgctl (int msqid ,int cmd ,struct msqid_ds*buf);

Demo 创建/获取消息队列

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

//int msgget (key,flag);
int msgget (0x1234 , IPC_CREAT | 0777);     //IPCC_CREAT标志位  如果由123队列就打开,没有就创建队列1234  0777是可读可写可执行权限

3、消息队列的使用

案例一:消息队列收发数据

Demo 查找/创建消息队列 并发送消息(假设key是写好的0x1234)

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>


struct msgBuf
{
   long mtype;
   char mtext[128];
};


int main(){

struct msgBuf readBuf;

int msgID = msgget(0x1234,IPC_CREAT | 0777);
if(msgID == -1){
     printf("get qun failuer\n");
}

msgrcv(msgID , &readBuf,sizeof(readBuf.mtext),888,0);
printf("read from qun :%s\n",readBuf.mtext);

return 0;
}

Demo查找/创建消息队列,并读取消息

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<string.h>


struct msgBuf
{
   long mtype;
   char mtext[128];
};


int main(){

struct msgBuf sendBuf ={888,"send context to qun\n"};


int msgID = msgget(0x1234,IPC_CREAT | 0777);
if(msgID == -1){
     printf("get qun failuer\n");
}

msgsnd(msgID , &sendBuf,strlen(sendBuf.mtext),0);

return 0;
}

运行结果

在这里插入图片描述

案例二:同时互相通信

Demo读取并发送消息

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<string.h>
struct msgBuf
{
   long mtype;
   char mtext[128];
};


int main(){


struct msgBuf readBuf;
struct msgBuf sendBuf={988,"Tranks for send!\n"};


int msgID = msgget(0x1234,IPC_CREAT | 0777);
if(msgID == -1){
     printf("get qun failuer\n");
}

msgrcv(msgID , &readBuf,sizeof(readBuf.mtext),888,0);
printf("read from qun :%s\n",readBuf.mtext);
msgsnd(msgID , &sendBuf,strlen(sendBuf.mtext),0);
    
return 0;
}  

发送并读取消息

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<string.h>

struct msgBuf
{
   long mtype;
   char mtext[128];
};

int main(){

struct msgBuf sendBuf ={888,"send context to qun\n"};
struct msgBuf readBuf;
int msgID = msgget(0x1234,IPC_CREAT | 0777);
if(msgID == -1){
     printf("get qun failuer\n");
}

msgsnd(msgID , &sendBuf,strlen(sendBuf.mtext),0);
msgrcv(msgID , &readBuf,sizeof(readBuf.mtext),988,0);
printf("read from get :%s\n",readBuf.mtext);
    
return 0;
}

运行结果
在这里插入图片描述

4、key的生成

key的生成需要用到ftok函数

ftok函数:

系统建立IPC通讯(消息队列,信号量和共享内存)时必须指定一个ID值,通常 情况下,该ID值通过ftok函数得到。

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok( const char * fname, int id )

参数说明

fname 就是你指定的文件名(已经存在的文件名),一般使用当前目录。
id 是子序号。虽然是int类型,但是只使用8bits(1-255)。

如:
key_t key;
key = ftok(“.”, 1);
这样就是将fname设为当前目录。
路径名使用的路径的索引节点。可以 ls -i 查看 文件索引节点号
通过 文件的索引节点号取出,前面加上序号得到key_t的返回值。

Demo

key_t key;
key=ftok(".","z");
printf("key"=%x\n,key);
int msgID = msgget(key ,IPC_CREAT | 0777);

5、消息队列的移除

//控制消息队列:成功返回0 ,失败返回-1
int msgctl (int msqid ,int cmd ,struct msqid_ds *buf);

msqid :队列ID
cmd : 指令

  • IPC_STAT
  • IPC_SET
  • IPC_RMID //把消息队列链表从内核中移除
  • IPC_INFO

buf : NULL

3.5 共享内存

1、共享内存概述

消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。

即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

特点:

  1. 共享内存是最快的一种IPC,因为进程是直接对内存进行存取。
  2. 因为多个进程可以同时操作,所以需要进行同步
  3. 一般 信号量 +共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

2、相关API

创建/打开共享内存

#include<sys/ipc.h>
#include<sys/shm.h>

int shmget(key_t key , size ,int flag);

参数说明

key: 键值
size: 共享内存的大小,必须以兆对齐 1024
flag : 打开队列方式 记得要 加上权限 0666 可读可写可执行

返回值: 成功返回共享内存ID,失败返回-1

映射(把共享内存挂载到进程的存储空间)

#include<sys/types.h>
#include<sys/shm.h>

void *shmat(int shm_id , void *addr , int flag);

参数说明:
id: 共享内存ID
addr: 0 表示,内核自动安排共享内存
flag : 打开队列方式 0 可读可写可执行

成功返回指向共享内存的指针

释放共享内存

#include<sys/types.h>
#include<sys/shm.h>

int shmdt(void *addr);

参数说明:
addr: 映射地址

控制共享内存

#include<sys/ipc.h>
#include<sys/shm.h>

int shmctl(int shm_id ,int cmd ,struct shmid_ds *buf)

参数说明:
id: 共享内存ID
cmd: 指令
buf : 0 一般写0 或者卸载信息

3、编程实现

Demo创建/打开共享内存 写入数据

#include<sys/types.h>
#include<sys/shm.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/ipc.h>

int main()
{

int shmid;
char *shmaddr;   //定义指针  用来指向共享内存

key_t key;
key = ftok(".",1);


shmid = shmget(key,1024*4,IPC_CREAT | 0666);  
 //创建/打开共享内存  0666权限
if(shmid == -1){
       printf("shmget errored\n");
       exit(-1);
}

shmaddr = shmat(shmid,0,0);  //映射到进程内存  shmaddr用来指向共享内存

printf("shmat is ok\n");

strcpy(shmaddr,"Refuel.CONG\n");  //写入数据
sleep(3);

shmdt(shmaddr);          //释放共享内存
shmctl(shmid,IPC_RMID,0);  //干掉共享内存
printf("quit\n");

return 0;
}

Demo创建/打开共享内存 并读取数据

#include<sys/types.h>
#include<sys/shm.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/ipc.h>

int main()
{

int shmid;
char *shmaddr;   //定义指针  用来指向共享内存

key_t key;
key = ftok(".",1);

shmid = shmget(key,1024*4,0);

shmaddr = shmat(shmid,0,0); //映射到进程内存  shmaddr用来指向共享内存

printf("shmat is ok\n");
printf("data:%s",shmaddr);  //读取数据

shmdt(shmaddr);    //释放共享内存

printf("quit\n");

return 0;
}

运行结果

在这里插入图片描述

Tips

1. 可以用 ipcs -m来查看系统中共享内存
在这里插入图片描述

2. 若要删除共享内存
ipcrm -m (要删除的共享内存ID号) 就可删除
如: ipcrm -m 8847339

3.6 信号

1、信号概述

对于linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号为linux提供了一种处理异步事件的方法。比如终端用户输入ctrl+c来中断程序,会通过信号机制停止一个程序。

信号的名字和编号: 每一个信号都有一个名字和编号,这些名字都以”SIG“开头,例如”SIGIO“、”SIGCHLD“等

在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:

# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。

例如
Ctrl+C 产生 SIGINT 信号,表示终止该进程;
Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;

如果进程在后台运行,可以通过 kill 命令的方式给进程发送信号,但前提需要知道运行中的进程 PID 号,例如:

kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号,用来立即结束该进程;

  1. 信号头文件:
#include<signal.h> 

信号名都定义为正整数。

具体的信号名称可以使用kill -l 来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。

信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

  1. 信号的处理有三种方式
  • 忽略

  • 捕捉

  • 默认动作

    分别说明三种方式:

  • 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(SIGKLL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就会变成没人管理的进程,显然这是不好的。

  • 捕捉信号,需要告诉内核。用户希望如何处理某一种信号,说简单就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。简单说就是我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。

  • 系统默认动作,对于每个信号来说,大部分的处理方法都比较粗暴,就是直接杀死该进程。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。

  1. 了解完信号的概述,那么,信号 如何来使用的呢?

如常用的kill命令就是一个发送信号的工具,kill 9 PID 来杀死进程
如:kill -9 9227 结果为:killed

对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段(捕捉信号),那如何来自定义信号的处理函数呢?
2、信号捕捉处理函数

信号处理函数的注册不只一种方法,分:入门和高级

  1. 入门:函数signal
 #include<signal.h>

typed void (*sighandler_t)(int);
sighandler_t signal(int signum , sighandler_t handler);

参数:

signum 信号

Demo 捕捉SIGINT信号 再修改SIGINT信号成printf(“dont quit \n”)

#include<signal.h>
#include<stdio.h>

void handler(int signum)
{   
      printf("get signum = %d\n",signum);
      printf("never quit\n");
}

int main(){
     signal(SIGINT,handler);
     while(1);
     return 0;
}

Demo 捕捉SIGINT SIGKILL SIGUSRL信号 修改成printf

#include<signal.h>
#include<stdio.h>


void handler(int signum)
{   
      printf("get signum = %d\n",signum);
      switch(signum){
             case 2: 
                    printf("SIGINT\n");
                    break;
             case 9:
                    printf("SIGKILL\n");
                    break;
              case 10:
                    printf("SIGUSR1\n");
                    break;
      }
      printf("never quit\n");
}


int main(){
     signal(SIGINT,handler);
     signal(SIGKILL,handler);
     signal(SIGUSR1,handler);
     while(1);
     return 0;
}

结果:除了SIGKILL都可以将信号修改成其他函数

Demo 捕捉SIGINT SIGKILL SIGUSRL信号 修改成SIG_IGN(忽略信号的宏)

#include<signal.h>
#include<stdio.h>

void handler(int signum)
{   
      printf("get signum = %d\n",signum);
      switch(signum){
             case 2:
                    printf("SIGINT\n");
                    break;
             case 9:
                    printf("SIGKILL\n");
                    break;
              case 10:
                    printf("SIGUSR1\n");
                    break;
      }
      printf("never quit\n");
}

int main(){
     signal(SIGINT,SIG_IGN);   //忽略信号SIGINT
     signal(SIGKILL,SIG_IGN);   //忽略信号SIGKILL
     signal(SIGUSR1,handler);  //忽略信号SIGUSR1
     while(1);
     return 0;
}

结果:除了SIGKILL以外,其他信号都可以被忽略

  1. 高级:sigaction
#inlcude<signal.h>

int sigaction(int signum ,struct sigaction *act ,struct sigaction *oldact);

参数说明:
signum:收哪个信号
sigaction (结构体): 收到信后干嘛
sigaction(结构体): 备份原有操作

sigaction结构体定义

struct  sigaction
{
void (*sa_handler)(int);  //信号处理程序,不接受额外数据,SIG_IGN为忽略
void  (*sa_sigaction)(int, siginfo_t * ,void *);   //信号处理程序,能够接收外数据
sigset_t  sa_mask;   //阻塞关键字的信号集 ,可以在调用捕捉函数之前,把信号添加到信号阻塞
int  sa_flags ;   //影响信号的行为SA_SIGINFO表示能够接收数据
};

sigaction (结构体)内容说明:
参数1:函数指针 *P1:void (*sa_handler)(int);

num P1不接收额外数据
参数2:函数指针 *P2:void (*sa_sigaction)(int, siginfo_t * ,void *);

signum
结构体:
1 pid:谁发的

  1. int si_int 数据
  2. si_value 联合体
    指针:
    1.空 :表示无数据
  3. 非空:表示有数据
    参数3:mark: 阻塞作用
    参数4:int flag:收取数据指定一个宏:SA_SIGINFO

回调函数句柄sa_handler、sa_sigaction只能选其一

3、信号处理发送函数

信号处理发送函数的注册不只一种方法,分:入门和高级

  1. 入门:函数kill

Demo 自定义 发送信号1 代替发信号kill

#include<signal.h>
#include<stdio.h>
#include<sys/types.h>

int main(int argc ,char **argv){
     int  signum;
     int pid;

     signum = atoi(argv[1]);  //字符串转换 atoi 表示 ASCII 转 整型数
     pid = atoi(argv[2]);
    
     printf("num =%d ,pid = %d\n",signum,pid);

     kill(pid,signum);
     printf("send signal ok\n");
     return 0;
}

Demo 自定义 发送信号2 代替发信号 kill

#include<signal.h>
#include<stdio.h>
#include<sys/types.h>


int main(int argc ,char **argv){
     int  signum;
     int pid;

     char cmd[128]={0};

     signum = atoi(argv[1]);    //字符串转换 atoi 表示 阿斯克吗 转 整型数
     pid = atoi(argv[2]);
    
     printf("num =%d ,pid = %d\n",signum,pid);

     sprintf(cmd =%d,pid=%d\n,signum,pid);
     
     system(cmd);
     printf("send signal ok\n");
     return 0;
}
  1. 高级:函数sigqueue
#include<signal.h>
int sigqueue(pid_t pid,int sig,const union sigval value);
//pid是目标进程的进程号
//sig是信号代号
//value参数是一个联合体,表示信号附带的数据,附带数据可以是一个整数也可以是一个指针,有如下形式:
union sigval {
int sival_int;
void *sival_ptr;//指向要传递的信号参数
};value

4. 高级函数信号携带消息实战

sigaction 高级获取信号

#include<signal.h>
#include<stdio.h>

//handler收到信号后
void handler(int signum , siginfo_t *info ,void *context){
   printf("get signum%d\n",signum);   //把信号值打出来


   if(context != NULL){  //判断指针是否为空,非空就有数据,把内容打出来
         printf("get data = %d\n",info->si_int);
         printf("get data = %d\n",info->si_value.sival_int);
         printf("from:%d\n",info->si_pid);   //发出者pid号
   }
}


int main(){
       
struct sigaction act;
printf("pid = %d\n",getpid());  //把ID号打出来

act.sa_sigaction = handler;   //收到信号后调用 handler结构体处理信号
act.sa_flags = SA_SIGINFO;  //接收信号必须指定宏SA_SIGINFO

sigaction(SIGUSR1,&act ,NULL);  //注册信号 参数1:收哪个信号  参数2:做什么 参数3:备份原有操作
while(1);
return 0 ;
}

sigqueue 高级发送信号

#include<stdio.h>
#include<signal.h>

int main(int argc ,char **argv)
{
    int signum;
    int  pid;

    signum = atoi(argv[1]);
    pid = atoi(argv[2]);

    union sigval value;
    value.sival_int = 100;
    
    sigqueue(pid,signum,value);
    printf("pid = %d\n",getpid());  //把ID号打出来
    printf("done\n");

return 0;

}

运行结果

在这里插入图片描述

3.7 信号量

1、信号量概述

信号量(semaphore )于已经介绍过的IPC结构不同,用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
信号量不涉及数据,是用来管理资源。

特点:

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
  2. 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作。
  3. 每次对信号量的PV操作不仅限于对信号量值加1 或者 减1,而且可以加减任意正整数。
  4. 支持信号量组。

临界资源:
多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用一次仅允许一个进程使用的资源称为临界资源许多物理设备都属于临界资源,如输入机、打印机、磁带机等。

2、相关API

原型:
简单的信号量是只能取0和1 的变量,这也是信号量最常见的一种形式,叫做 二值信号量(Binary Semaphore).而可以取多个正整数的信号量被称为 通用信号量

Linux下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作的。

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
//创建或获取一个信号量组:若成功返回信号量集ID 失败返回-1
int semget (key_t key , int num_sems ,int sem_flags );
//对信号量组进行操作,改变信号量的值    成功返回0 ,失败返回-1
int semop(int semid , struct sembuf *semoparray , size_t numops);
//控制信号量的相关信息
int semctl (int semid ,int sem_num ,int cmd ,...);

​ 在多进程里,每个进程并不一定是顺序执行的,它们基本是以各自独立的、不可预知的速度向前推进,但有时候我们又希望多个进程能密切合作,以实现一个共同的任务。例如,进程 A 是负责生产数据,而进程 B 是负责读取数据,这两个进程是相互合作、相互依赖的,进程 A 必须先生产了数据,进程 B 才能读取到数据,所以执行是有前后顺序的。那么这时候,就可以用信号量来实现多进程同步的方式,我们可以初始化信号量为 0。

Demo创建信号量

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<stdio.h>

//int  semget (key_t key , int num_sems ,int sem_flags );
//int semctl (int semid ,int sem_num ,int cmd ,......);

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


int main(int argc , char *argv[]){

  key_t key;
  int semid;


  key =ftok(".",2);   //信号量集合中有一个信号量


  semid = semget(key ,1 ,IPC_CREAT | 0666);  //获取/创建信号量
  //参数:1 信号量集合中有一个信号量
 
 union semun initsem;

  initsem.val = 0;  //表示没有钥匙(没有信号量)

  semctl(semid , 0 ,SETVAL,initsem); //初始化信号量
//参数1:semid 信号量ID
//参数2:操作第0个信号量
//参数3:SETVAL 设置信号量的值 ,设置为inisem

  int pid = fork();
  if(pid > 0){
     //去拿锁
     pritnf("this is tather \n");
     //把锁放回去
  }else if(pid == 0){

     printf("this is child\n");
  }else{
     printf("fork error\n");
  }

return 0;
}

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  1. p操作(拿钥匙)
  2. v操作(把钥匙放回去)
  3. P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。

p操作 //去拿锁(拿钥匙)
P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。

void  pGetkey(int id)
{
    struct sembuf set;

    set.sem_num =0;
    set.sem_op  = -1;
    set.sem_flag = SEM_UNDO;

    semop(if , &set , 1);

    printf("get key\n");
}

v操作 //把锁放回去(放钥匙)
V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

void  vPutbackkey(int id)
{
    struct sembuf set;

    set.sem_num =0;     //信号量编号
    set.sem_op  = 1;    //信号量编号
    set.sem_flg = SEM_UNDO;   //等待

    semop(if , &set , 1);

    printf("Putback the key\n");
}

Demo

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<stdio.h>


//int  semget (key_t key , int num_sems ,int sem_flags );
//int semctl (int semid ,int sem_num ,int cmd ,......);


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

void  pGetkey(int id)      //p操作
{
    struct sembuf set;

    set.sem_num =  0;    //信号量编号
    set.sem_op  = -1;    //信号量编号
    set.sem_flg = SEM_UNDO;    //等待

    semop(id , &set , 1);

    printf("get key\n");
}

void  vPutbackkey(int id)   //v操作
{
    struct sembuf set;

    set.sem_num = 0;    //信号量编号
    set.sem_op  = 1;    //信号量编号
    set.sem_flg = SEM_UNDO; //等待

    semop(id , &set , 1);

    printf("Putback the key\n");
}


int main(int argc , char *argv[]){

  key_t key;
  int semid;


  key = ftok(".",2);   


  semid = semget(key ,1 ,IPC_CREAT | 0666);  //获取/创建信号量
  //参数:1 信号量集合中有一个信号量
union semun initsem;


  initsem.val = 0;  //表示没有钥匙(没有信号量)


  semctl(semid , 0 ,SETVAL,initsem); //初始化信号量
//参数1:semid 信号量ID
//参数2:操作第0个信号量
//参数3:SETVAL 设置信号量的值 ,设置为inisem


  int pid = fork();
  if(pid > 0){
     pGetkey(semid);//去拿锁
     printf("this is tather \n");
     vPutbackkey(semid);//把锁放回去
  }else if(pid == 0){
 
     printf("this is child\n");
     vPutbackkey(semid);//把锁放回去  修改信号量

     semctl(semid , 0 ,IPC_RMID); //销毁 信号量
  }
   else{
       printf("fork error\n");
   }

return 0;
}

运行结果

父进程要进去,没有信号量,子进程把信号量放入,父进程拿着信号量,进入,出来又放回信号量。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux下,进程间通信的一种方式是通过共享内存来实现的。共享内存允许两个或多个进程共享一定的存储区,这样它们就可以直接访问同一块内存区域,而不需要进行数据的复制。共享内存是一种高效的进程间通信方式,因为数据直接写入内存,不需要多次数据拷贝,所以传输速度很快\[2\]。 在使用共享内存进行进程间通信时,需要给共享内存创建一个唯一的身份ID,以便区分不同的共享内存。当进程需要访问共享内存时,需要在映射时带上这个ID,这样就可以确定访问的是哪一个共享内存\[3\]。 需要注意的是,共享内存并没有提供同步机制,也就是说,在一个进程结束对共享内存的写操作之前,并没有自动机制可以阻止另一个进程开始对它进行读取。为了实现多个进程对共享内存的同步访问,通常会使用信号量来实现对共享内存的同步访问控制\[2\]。 总结起来,Linux下的共享内存是一种高效的进程间通信方式,允许多个进程共享一块存储区。通过给共享内存创建唯一的身份ID,可以区分不同的共享内存。然而,共享内存并没有提供同步机制,需要使用信号量来实现对共享内存的同步访问控制\[2\]\[3\]。 #### 引用[.reference_title] - *1* *3* [Linux进程间通信——共享内存实现](https://blog.csdn.net/zhm1949/article/details/124909541)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Linux进程间通信方式——共享内存](https://blog.csdn.net/xujianjun229/article/details/118584955)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值