进程间通信:IPC

介绍

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中Socket和Streams支持不同主机上的两个进程IPC

一、管道

管道,通常指无名管道,是UNIX系统IPC最古老的形式

  1. 特点
    1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写段
    2. 它只能用于具有亲缘关系的进程之间的通信,也就是父子或者兄弟进程之间
    3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存
  2. 原型

int pipe(int pipefd[2]); :如果创建成功,返回0,失败返回-1,同时传入的参数pipefd[0]变为读描述符,pipefd[1]变为写描述符

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int pip=0;
	int pipefd[2];
	char buf[1024];
	pip = pipe(pipefd);
	if(pip==-1){
		printf("error pip\n");
		return 0;
	}
	int pid = fork();//开启子进程
	
	if(pid == -1){
		printf("creat fork failure\n");
	}else if(pid == 0){
		printf("this is child process\n");
		close(pipefd[1]);//关闭子进程的写通道
        
		read(pipefd[0],buf,1024);
		printf("read father push: %s\n",buf);
		exit(0);

	}else if(pid > 0){

		printf("this is father process\n");
		close(pipefd[0]);//关闭父进程的读通道

		write(pipefd[1],"hello child process I is father process",strlen("hello child process I is father process"));
		wait();
	}
	return 0;
}

命名管道

  1. 特点
    1. FIFO可以在无关的进程之间交换数据,与无名管道不同,也就是说不拘泥于父子进程和兄弟进程
    2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
  2. 原型int mkfifo(const char* pathname,mode_t mode)man 3 mkfifo
    1. pathname:文件名
    2. mode:与open()中的mode相同,一旦创建了一个FIFO就可以用一般的文件I/O函数操作它
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
	int fifo = mkfifo("./file",0600);
	if(fifo == -1 && errno == EEXIST){
		printf("creat fifo failure\n");
		perror("why");
	}
	return;
}
  1. open一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别
    1. 若没有指定(默认),只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打开它
      1. 若指定了,则只读open立即返回。而只写open将出错返回-1,如果没有进程为读而打开该FIFO,其errno置ENXIO
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main()
{
        char* src = "./file";
        int fd = open(src,O_RDONLY);
        char buf[1024]={0};
        printf("open read\n");
        while(1){
                int n_read = read(fd,buf,1024);
                printf("read %d byte context:%s\n",n_read,buf);
                sleep(1);
        }
        close(fd);
        return 0;
}

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main()
{
        char* src = "./file";
        char* buf = "this is writeFile push data";
        int fd = open(src,O_WRONLY);
        if(fd < 0){
                printf("open fail\n");
                perror("why");
        }
        while(1){
                int n_write =write(fd,buf,strlen(buf));
                sleep(1);
        }
        close(fd);
        return 0;
}

消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来表示;其实我觉得按照实现方式来称呼为消息链表更合适

  1. 我们需要关心的事情
    1. 如何创建消息队列
    2. 一方如何将消息加到队列中
    3. 另一方如何从队列拿到消息
    4. 因为其余的事情是由具体的Linux内核所实现,我们不需要关心
  2. 特点
    1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
    2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会删除,由Linux内核的具体实现来进行管理
    3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
  3. 函数原型
    1. int msgget(key_t key, int msgflg);:创建或打开消息队列,返回队列ID,失败返回-1

      1. 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位
      2. key参数为IPC_PRIVATE。这两种情况就会创建一个新的消息队列
      3. 得到key值:key_t ftok(const char *pathname, int proj_id);
    2. int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);:添加消息,成功返回0,失败返回-1

    3. ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);:读取消息,成功返回消息数据的长度,失败返回-1

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

image.png

  1. 示例代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdio.h>
//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
struct msgbuf{
        long mtype;
        char mtext[1024];
};
int main()
{
        key_t key = ftok(".",23);
        printf("key value:%x\n",key);
        int msg_que_id = msgget(key,IPC_CREAT|0777);

        struct msgbuf buf = {8,"this is send push buf"};

        msgsnd(msg_que_id,&buf,strlen(buf.mtext),0);
        printf("send success\n");
        msgctl(msg_que_id,IPC_RMID,NULL);
        return 0;
}

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
//key_t ftok(const char *pathname, int proj_id);

struct msgbuf{
        long mtype;
        char mtext[1024];
};

int main()
{
        key_t key = ftok(".",23);
        printf("key value:%x\n",key);

        int msg_que_id = msgget(key,IPC_CREAT|0777);

        struct msgbuf buf;

        msgrcv(msg_que_id,&buf,sizeof(buf.mtext),8,0);
        printf("receive msg:%s\n",buf.mtext);

        msgctl(msg_que_id,IPC_RMID,NULL);
        return 0;
}

共享内存

使用步骤

  1. 创建共享/打开:int shmget(key_t key, size_t size, int shmflg);:成功返回共享内存ID,失败返回-1
  2. 映射:void *shmat(int shmid, const void *shmaddr, int shmflg);:成功返回指向共享内存的指针,失败返回-1,第二个参数如果为0,则为内核自动分配内存
  3. 写入/读取数据
  4. 释放共享内存:int shmdt(const void *shmaddr);:成功返回0,失败返回-1
  5. 删除共享内存:int shmctl(int shmid, int cmd, struct shmid_ds *buf);:控制共享内存的相关信息,成功返回0,失败返回-1
  6. 命令查看共享内存:ipcs -m
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
        //obtain key
        key_t key = ftok(".",12);
        //obtain share id
        int share_id = shmget(key,1024*4,IPC_CREAT|0600);
        //mapping address
        void *map_address = shmat(share_id,0,0);
        printf("shmat:OK\n");
        strcpy((char*)map_address,"this is write share");
        //release share address
        shmdt(map_address);
        //delete share address
        shmctl(share_id,IPC_RMID,0);
        return 0;
}

<string.h>

int main()
{
        //obtain key
        key_t key = ftok(".",12);
        //obtain share id
        int share_id = shmget(key,1024*4,IPC_CREAT|0600);
        //mapping address
        void *map_address = shmat(share_id,0,0);
        sleep(5);//阻塞等待写入
        printf("shmat:OK\n");
        printf("this is read:%s\n",(char*)map_address);
        //release share address
        shmdt(map_address);
        //delete share address
        shmctl(share_id,IPC_RMID,0);

        return 0;
}

Linux信号:软中断

信号概述

  1. 信号的名字和编号:以SIG开头,定义在signal.h头文件中,信号名都定义为正整数,具体的信号名称可以使用kill -l 来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号,kill对于0信号有特殊的应用
  2. 信号的处理:忽略、捕捉和默认动作
    1. 忽略信号,大多数信号可以使用这个方式来处理,担忧有两种信号不能被忽略:SIGKILL和SIGSTOP,如果忽略了,那么这个进程就变成了没人能管理的进程
    2. 捕捉信号(主要使用这一点,来实现异步通讯):写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理
    3. 系统默认动作,对于每个信号来说,系统都对应有默认的处理动作,当发送了该信号,系统会自动执行。具体的信号默认动作可以使用man 7 signal 来查看系统的具体定义,或《UNIX环境高级系统编程》
  3. 信号处理函数的注册
    1. 入门:signal()
    2. 高级:int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);也就是能处理消息
  4. 信号处理发送函数
    1. 入门:kill()
    2. 高级:sigqueue(),也就是能携带消息
  5. 简单例子——捕捉信号:
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>

int main(int argc,char** argv)
{
        int pid =  atoi(argv[2]);
        int sid = atoi(argv[1]);
        printf("pid=%d,sid=%d\n",pid,sid);
        char cmd[128]={0};
        //kill(pid,sid);
        sprintf(cmd,"kill -%d %d",sid,pid);
        system(cmd);
        return 0;
}

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

void handler(int signal)
{
        printf("get signal=%d\n",signal);
        switch(signal){
                case SIGINT:
                        printf("current SIGINT");
                        break;
                case SIGKILL:
                        printf("current SIGKILL");//实际上无法生效
                        break;
                case SIGUSR1:
                        printf("current SIGUSR1");
                        break;
        }
        printf("never\n");
}

int main(int argc,char** argv)
{
        //signal(SIGINT,SIG_IGN);忽略SIGINT信号
        signal(SIGINT,handler);
        signal(SIGKILL,handler);
        signal(SIGUSR1,handler);
        //int pid =  argv[1];
        //int sid = argv[2];
        while(1);
        return 0;
}

信号如何携带消息

  1. 发信号
    1. 用什么发
    2. 怎么放入消息
  2. 收信号
    1. 用什么绑定函数:int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
      1. 第一个参数为信号的编号
      2. act是一个指向sigaction结构体的指针类型参数,用于设置新的信号处理函数,如果act指针不为空,则会将act中定义的新信号处理函数注册到signum对应的信号上;否则表示此次调用只是查询或者修改现有的信号处理函数,而不需要更改sigaction结构体中的内容
      3. oldact也是一个指向sigaction结构体的指针类型参数,用于存储之前的信号处理函数,即在调用sigaction函数时旧的信号处理函数会被复制到oldact指向的地址中。
      4. struct sigaction结构体:用于定义信号的处理方式包含如下:
struct sigaction {
    void (*sa_handler)(int);   // 指向函数的指针类型,表示捕获到信号后要执行的操作
    void (*sa_sigaction)(int, siginfo_t *, void *); // 指向函数的指针类型,带有三个参数:int、siginfo_t*和void*
    sigset_t sa_mask;          // 需要在执行信号处理函数之前屏蔽的信号集合
    int sa_flags;              // 指定了一些标志位,例如SA_RESTART或SA_NOCLDSTOP等
    void (*sa_restorer)(void); // 指向函数的指针类型,用于恢复系统调用被中断时的上下文环境
}
  • sa_handler是一个指向函数的指针类型,用于指定信号处理函数,并且它不带额外的信息。当信号发生时,操作系统将自动调用sa_handler所指定的函数。
  • sa_sigaction也是一个指向函数的指针类型,与sa_handler不同的是,它能够接收到关于信号的更多信息,例如信号来源、信号传递的附加数据等等。因此,在需要获取额外信息的场合下,使用sa_sigaction更为灵活。该函数的三个参数解析如下
    • 第一个参数表示信号的编号
    • 第二个参数是一个指针,提供了有关发出信号的更多信息,如信号来源等,传递给信号处理函数的数据,以及其他的一些信息。
    • 第三个参数包含一些额外的数据,这个参数不会被内核修改,而且只能由发送者在用户空间中设置。如描述段错误原因的信息、内存地址或其他上下文信息等。

image.png

  • sa_mask成员变量是信号掩码,用于设置在执行信号处理函数期间需要被屏蔽的信号集合。在执行信号处理函数期间,系统将不会响应被屏蔽的信号。
  • sa_flags成员变量是用于设置一些标志位的,例如SA_RESTART指定了当系统调用被中断时自动重新启动这个系统调用,SA_NOCLDSTOP表示在子进程停止或继续运行时不会通知父进程等等。
  • sa_restorer成员变量是一个指向函数的指针类型,它用于恢复在信号处理函数中被中断的系统调用。在有些情况下,在执行完信号处理函数之后,需要恢复被中断的系统调用,以使程序正常执行。

image.png

  1. 如何读出消息
    1. int sigqueue(pid_t pid, int sig, const union sigval value);
    2. 第一个参数为:发给谁
    3. 发送什么信号
    4. 数据
  2. 示例代码1
#include <signal.h>
#include <stdio.h>
// int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)
//sigval_t si_value;
void handler(int signum,siginfo_t* info,void* context)
{
        printf("signum=%d\n",signum);
        if(signum == SIGUSR1 && context!=NULL){
                printf("current process_pid=%d\n",getpid());
                printf("info_pid=%d\n",info->si_pid);
                printf("info_si_value=%d\n",info->si_value.sival_int);
        }
}
int main()
{
        struct sigaction act;

        act.sa_sigaction = handler;
        act.sa_flags=SA_SIGINFO;

        sigaction(SIGUSR1,&act,NULL);

        while(1);
        return 0;
}

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

// int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)
//sigval_t si_value;
//int sigqueue(pid_t pid, int sig, const union sigval value);
int main(int argc,char** argv)
{
        union sigval value;
        value.sival_int = 100;
        pid_t pid = atoi(argv[2]);
        int sid = atoi(argv[1]);
        sigqueue(pid,sid,value);
        printf("current pid=%d\n",getpid());
        return 0;
}

实战的一些问题

:::info
对于代码中的对context判空,似乎不太理想,当使用kill发送信号的时候,context依然不为空,也就是说,无论是否携带数据,context都不为空,所以对于通过判断context是否为空,来判断是否携带数据,有点不成立
:::

信号量

信号量是一个计数器,用于实现进程间的互斥与同步,而不是存储进程间通信数据

临界资源:多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅运行一个进程使用的资源称为临界资源

信号量集:总共有多少的锁
P操作:拿锁
V操作:放回锁

相关函数

  1. int semget(key_t key,int num_sems, int sem_flags);:创建或获取一个信号量组,若成功则返回信号量集ID,失败返回-1
  2. int semop(int semid,struct sembuf semoparrayp[],size_t numops);:对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
  3. int semctl(int semid,int sem_num,int cmd,..);:控制信号量的相关信息。第三个参数:指定信号量操作的数量。nsops参数应该大于等于1,它表示sembuf数组中需要执行的操作个数。如果有多个操作需要进行,则应该将它们按顺序放在sembuf数组中,并设置nsops参数为结构体数组的长度。

相关结构体

struct sembuf{
	unsigned short sem_num;  // 信号量在信号量集中的编号
	short          sem_op;   // 信号量操作:增加、减少或者等待0
	short          sem_flg;  // 操作标志:IPC_NOWAIT(非阻塞) 或 0(阻塞) 
}
/*其中,sem_num表示要操作的信号量在信号量集中的位置;
sem_op为操作类型,可以是正值(增加信号量值)、负值(减少信号量值)或零(等待信号量值为0,即锁住信号量)。
sem_flg为操作标志,它决定了semop()函数的调用方式,可以是IPC_NOWAIT(非阻塞模式),或者0(阻塞模式)。*/

示例代码

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

union semun {
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
};
void pGetKey(int id)
{	
	struct sembuf sops;
	sops.sem_num=0;
	sops.sem_op=-1;
	sops.sem_flg = SEM_UNDO;
	semop(id,&sops,1);
	printf("getKey success\n");
}

void vBackKey(int id)
{
	struct sembuf sops;
	sops.sem_num=0;
	sops.sem_op=1;
	sops.sem_flg = SEM_UNDO;
	semop(id,&sops,1);
	printf("vBackKey success\n");
}

int main()
{
	key_t key = ftok(".",1);
	int semid = semget(key,1,IPC_CREAT|0600);
	union semun sem;
	sem.val = 0;
	semctl(semid,0,SETVAL,sem);
	int pid = fork();
	if(pid > 0){
		pGetKey(semid);
		printf("this is father\n");
		vBackKey(semid);
	}else if(pid == 0){
		printf("this is chail\n");
		vBackKey(semid);
	}else {
		printf("creat process error\n");
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值