Linux-进程间的通信

1、IPC:

Inter Process Communication(进程间通信):

由于每个进程在操作系统中有独立的地址空间,它们不能像线程那样直接访问彼此的内存,所以必须通过某种方式进行通信。

常见的 IPC 方式包括: 2、无名管道:

管道的概念:

本质:

1、内核缓冲区;

2、伪文件-不占用磁盘空间;

特点:

1、读端,写端对应两个文件描述符;

2、数据写端流入,读端流出;

3、操作管理的进程被销毁之后,管道自动被释放;

4、管道默认式阻塞的;

创建匿名管道:

示例:

cpp 复制代码
int pipe(int fd[2])

fd‐传出参数:
fd[0]‐读端
fd[1]‐写端

返回值:
0:成功
‐1:创建失败

实验:父子进程使用管道进行通信,实现 ps aux | grep "bash":

使用父子进程管道通信时读端和写端不能同时打开,父进程打开写端关闭读端;

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>

int main()
{

        int ret;
        int fd[2];
        pid_t pid;
        ret = pipe(fd);
        pid  = fork();
        if(ret == -1)
        {
                printf("create pipe failed!");
                exit(1);
        }
        //ps aux
        if(pid>0)
        {
                close(fd[0]);//guanbi du duan da kai xie duan
                dup2(fd[1],STDOUT_FILENO);
                execlp("ps","ps","aux",NULL);
                exit(1);

        }
        //grep bush
        if(pid==0)
        {

                close(fd[1]);//guanbi xieduan da kai duduan
                dup2(fd[0],STDIN_FILENO);
                execlp("grep","grep","bush",NULL);

        }

        printf("pipe[0] is %d\n",fd[0]);
        printf("pipe[1] is %d\n",fd[1]);

        return 0;
}
~            

结果:

3、有名管道:

示例:

cpp 复制代码
int mkfifo(const char \*filename, mode_t mode)

功能: 创建管道文件;

参数: 管道文件名,权限,创建的文件权限和umask有关;

返回值: 创建成功返回0,创建失败返回-1;

特点:

有名管道;

伪文件,不占用磁盘空间;

半双工通信方式;

使用场景:

用于实现无血缘关系的进程间通信,使用mkfifo函数只会在用户区创建一个节点,不会在内核区创建管道,需要使用IO函数OPEN函数打开管道;

cpp 复制代码
ret = mkfifo("/home/study/ipc/mkfifo",0755);
 
fd = open("./mkfifo",O_RDONLY);

实验:创建一个管道,让进程A写入进程B读出:

cpp 复制代码
读进程:

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

int main()
{
        int ret;
        int fd;
        int nred;
        char readbuf[50]={0};
        //ret = mkfifo("/home/study/ipc/mufifo",0755);

        if(ret == -1)
        {
                printf("filed!");

        }
        printf("create success\n");

        fd = open("./mufifo",O_RDONLY);
        if(fd == -1)
        {
                printf("open filed\n");
        }
        printf("open success\n");

        nred = read(fd,readbuf,50);
        printf("read %d byte from fifo %s\n",nred,readbuf);
        printf("second read %d byte from fifo %s\n",nred,readbuf);
        close(fd);
        return 0;
}
~    
cpp 复制代码
写进程:

#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
        char *str = "hello world";
        int fd;
        int ret;
        ret = mkfifo("/home/study/ipc/mufifo",0755);


        fd = open("./mufifo",O_WRONLY);

        if(fd < 0)
        {
                return -1;
        }
        printf("open fifo success\n");

        write(fd,str,strlen(str));

        close(fd);
        return 0;

}                                                                                                                                                                                                                                                                                                                                                           

结果:

注意:管道中的信息只能读取一次,读取完之后数据就被移除,但是管道还存在;

4、消息队列:

概念:

消息队列,是消息的链表,存放在内核中,一个消息队列由一个标识符(队列ID)来标识;

特点:

消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;

消息队列独立于发送和接收进程,进程终止时,消息队列及其内容仍存在;

消息队列可以实现消息的随即查询,消息不一定要先进先出的次序读取,也可以按照消息类型进行读取;

两个进程间的通信:通过识别链表的ID来进行读写实现收发;

相关函数:

cpp 复制代码
1、int msgget(key_t key, int msgflg);
//创建或打开消息队列,
参数:
key:和消息队列关联的key值
msgflg:是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或
操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被
忽略,而只返回一个标识符。
返回值:成功返回队列ID,失败则返回‐1,

在以下两种情况下,msgget将创建一个新的消息队列:
如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志
key参数为IPC_PRIVATE
cpp 复制代码
2.int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//读取消息,成功返回消息数据的长度,失败返回‐1
参数:
msgid:消息队列的ID
msgp:指向消息的指针,常用结构体msgbuf如下:
struct msgbuf
{
long mtype; //消息类型
char mtext[N]; //消息正文
}
size:发送的消息正文你的字节数
flag:
IPC_NOWAIT 消息没有发送完成函数也会立即返回0:知道发送完成函数才返回
返回值:
成功:0
失败:‐1
cpp 复制代码
3.ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//从一个消息队列中获取消息
参数:
msgid:消息队列的ID
msgp:要接收消息的缓冲区
size:要接收的消息的字节数
msgtype:
0:接收消息队列中第一个消息
大于0:接收消息队列中第一个类型为msgtyp的消息
小于0:接收消息队列中类型值不大于msgtyp的绝对值且类型值又最小的消息。
flag:
0:若无消息函数一直阻塞
IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG。
返回值:
成功:接收到的消息i长度
出错:‐1

函数msgrcv在读取消息队列时,type参数有下面几种情况
type ==0,返回队列中的第一消息
type >0,返回队列中消息队列类型为type的第一个消息
type <0,返回队列中消息类型值小于或等于type绝对值的消息,如果有多个,则取类型值最小的消息。
可以看出,type值非0时用于以非先进先出次序读取消息,也可以把type看成优先级的权值
cpp 复制代码
4.int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//控制消息队列,成功返回0,失败返回‐1
参数:
msqid:消息队列的队列ID
cmd:
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆
盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
IPC_RMID:删除消息队列
buf:是指向 msgid_ds 结构的指针,它指向消息队列模式和访问权限的结构
返回值:
成功:0
失败:‐1
cpp 复制代码
ftok函数
key_t ftok( char * fname, int id )
//系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
参数:
fname就时你指定的文件名(该文件必须是存在而且可以访问的)。
id是子序号, 虽然为int,但是只有8个比特被使用(0‐255)。
返回值:
当成功执行的时候,一个key_t值将会被返回,否则 ‐1 被返回

4、共享内存:

概念:

共享内存(Shared Memory)就是允许多个进程访问同一个内存空间,是在多个进程之间共享和传递数据最高效的方式。操作系统将不同进程之间共享内存安排为同一段物理内存,进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也将会改变;

共享内存会从内核区映射一个地址空间到用户区,需要访问用户区的地址空间才能实现读写功能;

相关函数:

cpp 复制代码
1.int shmget(key_t key, size_t size, int shmflg);
//用来获取或创建共享内存
参数:
key:IPC_PRIVATE 或 ftok的返回值
size:共享内存区大小
shmflg:同open函数的权限位,也可以用8进制表示法
返回值:
成功:共享内存段标识符‐‐‐ID‐‐‐文件描述符
出错:‐1
cpp 复制代码
2.void *shmat(int shm_id, const void *shm_addr, int shmflg);
//把共享内存连接映射到当前进程的地址空间
参数:
shm_id:ID号
shm_addr:映射到的地址,NULL为系统自动完成的映射
shmflg:
SHM_RDONLY共享内存只读
默认是0,表示共享内存可读写
返回值:
成功:映射后的地址
失败:NULL
cpp 复制代码
3.int shmdt(const void *shmaddr);
//将进程里的地址映射删除
参数:
shmid:要操作的共享内存标识符
返回值:
成功:0
出错:‐1
cpp 复制代码
4.int shmctl(int shm_id, int command, struct shmid_ds *buf);
//删除共享内存对象
参数:
shmid:要操作的共享内存标识符
cmd :
IPC_STAT (获取对象属性)‐‐‐ 实现了命令ipcs ‐m
IPC_SET (设置对象属性)
IPC_RMID (删除对象) ‐‐‐实现了命令ipcrm ‐m
buf :指定IPC_STAT/IPC_SET时用以保存/设置属性
返回值:
成功:0
出错:‐1

实验:生成共享内存,并通过标准输入写入共享内存,并读出来打印屏幕上;

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <unistd.h>      // fork()
#include <sys/ipc.h>     // 共享内存所需
#include <sys/shm.h>     // shmget, shmat

int main()
{
        int shmid;
        int key;
        char *p;
        key = ftok("alarm.c",1);
        if(key<0)
        {
                printf("ftok failure\n");
                return -1;
        }
        printf("ftok success key:%x\n",key);
        shmid = shmget(key,128,IPC_CREAT|0777);
        if(shmid<0)
        {
                printf("create failure\n");
                return -2;
        }
        printf("create succ");
        system("ipcs -m");

        p = (char *)shmat(shmid,NULL,0);
        if(p = NULL)
        {
                printf("shmat funtion failure\n");
                return -3;
        }
        //write to
        fgets(p,128,stdin);
        //read
        printf("share data is %s",p);
        return 0;
}

结果:

5、信号:

信号通信的框架:

cpp 复制代码
信号通信的框架:
    信号的发送(发送信号的进程):kill、raise、alarm
    信号的接收(接收信号的进程):pause()、sleep、while(1)
    信号的处理(接收信号的进程):signal
5.1、信号的发送(发送信号的进程):
cpp 复制代码
1、kill:

#include<signal.h>
#include<sys/types.h>
函数原型:int kill(pid_t pid, int sig);
参数:
函数传入值:pid
正数:要接收信号的进程的进程号
0:信号被发送到所有和pid进程在同一个进程组的进程
‐1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
sig:信号
函数返回值:成功 0 出错 ‐1
cpp 复制代码
2、raise:发送信号给自己 == kill(getpid() , sig)

所需头文件:
#include<signal.h>
#include<sys/types.h>
函数原型:
int raise(int sig);
参数:
函数传入值:sig:信号
函数返回值:
成功 0 出错 ‐1

实验 :raise==kill(getpid(),sig):

cpp 复制代码
#include<stdio.h>
#include <signal.h>


int main()
{

        printf("before sig\n");
        raise(9);
        printf("after sig\n");
        return 0 ;
}
~    

结果: 打印完"before sig"后进程执行kill动作,结束进程,不会打印"after sig";

5.2、alarm:发送闹钟信号的函数:

5.2.1、alarm与raise函数的比较:

相同点:让内核发送信号给进程

不同点:alarm只会发送SIGALARM信号;

alarm会让内核定时一段时间在发送信号,raise会立即发送信号;

cpp 复制代码
所需头文件#include <unistd.h>
函数原型 unsigned int alarm(unsigned int seconds)
参数:
seconds:指定秒数
返回值:
成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则 返回上一个闹钟时间的剩余时间,否则返回0。
出错:‐1

实验:定时7秒向进程发送信号:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{

        int i = 0;
        printf("before alarm\n");
        alarm(7);
        printf("after alarm\n");
        while(i<20)
        {
                i++;
                sleep(1);
                printf("process %d\n",i);

        }
        return 0;

}
                                                                                               
                                                                                                 

结果:

在第7秒时想进程发送信号,但是由于没有设置信号处理函数对这个信号进行处理,所以这个信号会让进程终止;

信号的含义及默认操作:

信号名 含义 默认操作
SIGHUP 该信号在用户终端连接 ( 正常或非正常 ) 结束时发出,通常是在终端的控制进程结束 时,通知同一会话内的各个作业与控制终端不再关联。 终止
SIGINT 该信号在用户键入 INTR 字符 ( 通常是 Ctrl-C) 时发出,终端驱动程序发送此信号并送到 前台进程中的每一个进程。 终止
SIGQUIT 该信号和 SIGINT 类似,但由 QUIT 字符 ( 通常是 Ctrl-) 来控制。 终止
SIGILL 该信号在一个进程企图执行一条非法指令时 ( 可执行文件本身出现错误,或者试图执 行数据段、堆栈溢出时) 发出。 终止
SIGFPE 该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出 及除数为0 等其它所有的算术的错误。 终止
SIGKILL 该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略 终止
SIGALRM 该信号当一个定时器到时的时候发出。 终止
SIGSTOP 该信号用于暂停一个进程,且不能被阻塞、处理或忽略 暂停进程
SIGTSTP 该信号用于暂停交互进程,用户可键入 SUSP 字符 ( 通常是 Ctrl-Z) 发出这个信号 暂停进程
SIGCHLD 子进程改变状态时,父进程会收到这个信号 忽略
SIGABORT 该信号用于结束进程 终止
5.3、信号的处理:

收到信号的进程,应该怎样处理?

处理的方式:

1、进程的默认处理(内核为用户进程设置的默认处理方式)

A:忽略、B:终止进程、C:暂停

2、自己的处理方式:自己处理信号的方法告诉内核,这样你的进程收到了这个信号就会采用你自己的的处理方式

cpp 复制代码
所需头文件 #include <signal.h>
函数原型 void (*signal(int signum, void (*handler)(int)))(int);
函数传入值
signum:指定信号
handler
SIG_IGN:忽略该信号。
SIG_DFL:采用系统默认方式处理信号
自定义的信号处理函数指针
函数返回值
成功:设置之前的信号处理方式
出错:‐1
signal 函数有二个参数,第一个参数是一个整形变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函
数;这个函数的返回值是一个函数指针。

6、信号灯

信号灯集合(可以包含多个信号灯)IPC对象是一个信号的集合(多个信号量)

cpp 复制代码
1、semget

函数原型: int semget(key_t key, int nsems, int semflg);
//创建一个新的信号量或获取一个已经存在的信号量的键值。
所需头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数参数:
key:和信号灯集关联的key值
nsems: 信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限
函数返回值:
成功:信号灯集ID
出错:‐1
cpp 复制代码
2、semctl:

函数原型:int semctl ( int semid, int semnum, int cmd,...union semun arg(不是地址));
//控制信号量,删除信号量或初始化信号量
所需头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数参数:
semid:信号灯集ID
semnum: 要修改的信号灯编号
cmd :
GETVAL:获取信号灯的值
SETVAL:设置信号灯的值
IPC_RMID:从系统中删除信号灯集合
函数返回值:
成功:0
出错:‐1
cpp 复制代码
3、P V 操作:

int semop(int semid ,struct sembuf *sops ,size_t nsops);
//用户改变信号量的值。也就是使用资源还是释放资源使用权
包含头文件:
include<sys/sem.h>
参数:
semid : 信号量的标识码。也就是semget()的返回值
sops是一个指向结构体数组的指针。
struct sembuf{
unsigned short sem_num;//信号灯编号;
short sem_op;//对该信号量的操作。‐1 ,P操作,1 ,V操作
short sem_flg;0阻塞,1非阻塞
};
sem_op : 操作信号灯的个数
//如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负
数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用
权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。

实验: 通过信号灯操作和共享内存来共同实现父子进程通信:

实现思路:

1、**声明 P V 操作并进行初始化:**p v操作需要配置函数,在使用P V都可以这样进行配置

cpp 复制代码
void Poperation(int index,int semid)
{
        struct sembuf sop;
        sop.sem_num = index;
        sop.sem_op = -1;
        sop.sem_flag = 0;

        semop(semid,&sop,1);


}

void Voperation(int index,int semid)
{
        struct sembuf sop;
        sop.sem_num = index;
        sop.sem_op = 1;
        sop.sem_flg = 0;

        semop(semid,&sop,1);


}

2、使用union semun初始化信号量:常与semctl()函数一起使用

cpp 复制代码
union semun
{

        int val;
}

3、生成共享内存:

cpp 复制代码
 int shmid; 
 char *shmaddr;       
 shmid = shmget(key,128,IPC_CREAT|0755);

4、宏定义读写两个信号量:

cpp 复制代码
#define SEM_WRITE 1
#define SEM_READ 0

5、 使用semctl函数来控制读写两个信号量:

cpp 复制代码
 //init semaphore
        union semum myun;
        //init semaphore read
        myun.val=0;
        semctl(semid,SEM_READ,SETVAL,myun);
        //init semaphore write
        myun.val = 1;
        semctl(semid,SEM_WRITE,SETVAL,myun);

6、生成父子进程,并分别实现父子进程的动作:

父进程实现从键盘输入,并执行写操作,读操作暂停:

子进程执行读操作,写操作暂停;

cpp 复制代码
       pid_t pid;
       pid = fork();
        if(pid == 0)//child process
        {
                while(1)
                {
                        shmaddr = (char *)shmat(shmid,NULL,0);

                        Poperation(SEM_READ,semid);
                        printf("get share memory is %s\n",shmaddr);
                        Voperation(SEM_WRITE,semid);
                }
        }
        else if(pid>0)//father process
        {
                while(1)
                {
                        shmaddr =( char *)shmat(shmid,NULL,0);

                        Poperation(SEM_WRITE,semid);
                        printf("please input to share memory\n");
                        fgets(shmaddr,32,stdin);
                        Voperation(SEM_READ,semid);
                }

        }

完整代码:

cpp 复制代码
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

#define SEM_WRITE 1
#define SEM_READ 0


//ding yi xin hao lian he ti 
// 必须手动定义 semun union,因为它不在标准头文件中定义
union semun {
    int val;                // 用于 SETVAL
    struct semid_ds *buf;   // 用于 IPC_STAT 和 IPC_SET
    unsigned short *array;  // 用于 GETALL 和 SETALL
    struct seminfo *__buf;  // 用于 IPC_INFO(可选,可以忽略)
};


void Poperation(int index,int semid)
{
        struct sembuf sop;
        sop.sem_num = index;
        sop.sem_op = -1;
        sop.sem_flg = 0;

        semop(semid,&sop,1);


}

void Voperation(int index,int semid)
{
        struct sembuf sop;
        sop.sem_num = index;
        sop.sem_op = 1;
        sop.sem_flg = 0;
        semop(semid,&sop,1);


}

void Voperation(int index,int semid)
{
        struct sembuf sop;
        sop.sem_num = index;
        sop.sem_op = 1;
        sop.sem_flg = 0;

        semop(semid,&sop,1);


}

int main()
{

        key_t key;
        key = ftok(".",123);
        if(key == -1)
        {
                perror("ftok");
                return -1;
        }
        int semid;
        int shmid;
        char *shmaddr;
        semid = semget(key,2,IPC_CREAT|0755);
        if(semid<0)
        {
                perror("semget");
                return -1;
        }
        shmid = shmget(key,128,IPC_CREAT|0755);
        if(shmid<0)
        {

                perror("shmget");
                return -2;
 }
        //init semaphore
        union semun myun;
        //init semaphore read
        myun.val=0;
        semctl(semid,SEM_READ,SETVAL,myun);
        //init semaphore write
        myun.val = 1;
        semctl(semid,SEM_WRITE,SETVAL,myun);

        pid_t pid;
        pid = fork();
        if(pid == 0)//child process
        {
                while(1)
                {
                        shmaddr = (char *)shmat(shmid,NULL,0);

                        Poperation(SEM_READ,semid);
                        printf("get share memory is %s\n",shmaddr);
                        Voperation(SEM_WRITE,semid);
                }
        }
        else if(pid>0)//father process
        {
                while(1)
                {
                        shmaddr =( char *)shmat(shmid,NULL,0);

                        Poperation(SEM_WRITE,semid);
                        printf("please input to share memory\n");
                        fgets(shmaddr,32,stdin);
                        Voperation(SEM_READ,semid);
                }

        }
        return 0;


}

结果:父子进程就会交替执行读写操作,并不会同时占用共享内存:

相关推荐
律宏阔2 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔2 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴2 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千2 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei372 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei372 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei372 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh2 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW2 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
百万蹄蹄向前冲2 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js