介绍
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中Socket和Streams支持不同主机上的两个进程IPC
一、管道
管道,通常指无名管道,是UNIX系统IPC最古老的形式
- 特点
- 它是半双工的(即数据只能在一个方向上流动),具有
固定的读端和写段
- 它只能用于具有亲缘关系的进程之间的通信,也就是父子或者兄弟进程之间
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且
只存在于内存
中
- 它是半双工的(即数据只能在一个方向上流动),具有
- 原型
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;
}
命名管道
- 特点
- FIFO可以在无关的进程之间交换数据,与无名管道不同,也就是说不拘泥于父子进程和兄弟进程
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
- 原型
int mkfifo(const char* pathname,mode_t mode)
:man 3 mkfifo
- pathname:文件名
- 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;
}
- 当
open
一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别- 若没有指定(默认),只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打开它
- 若指定了,则只读open立即返回。而只写open将出错返回-1,如果没有进程为读而打开该FIFO,其errno置ENXIO
- 若没有指定(默认),只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打开它
#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)来表示;其实我觉得按照实现方式来称呼为消息链表更合适
- 我们需要关心的事情
- 如何创建消息队列
- 一方如何将消息加到队列中
- 另一方如何从队列拿到消息
- 因为其余的事情是由具体的Linux内核所实现,我们不需要关心
- 特点
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会删除,由Linux内核的具体实现来进行管理
- 消息队列可以实现消息的
随机查询
,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
- 函数原型
-
int msgget(key_t key, int msgflg);
:创建或打开消息队列,返回队列ID,失败返回-1- 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位
- key参数为IPC_PRIVATE。这两种情况就会创建一个新的消息队列
- 得到key值:
key_t ftok(const char *pathname, int proj_id);
-
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
:添加消息,成功返回0,失败返回-1 -
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
:读取消息,成功返回消息数据的长度,失败返回-1 -
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
:控制消息队列,成功返回0,失败返回-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;
}
共享内存
使用步骤
- 创建共享/打开:
int shmget(key_t key, size_t size, int shmflg);
:成功返回共享内存ID,失败返回-1 - 映射:
void *shmat(int shmid, const void *shmaddr, int shmflg);
:成功返回指向共享内存的指针,失败返回-1,第二个参数如果为0,则为内核自动分配内存 - 写入/读取数据
- 释放共享内存:
int shmdt(const void *shmaddr);
:成功返回0,失败返回-1 - 删除共享内存:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
:控制共享内存的相关信息,成功返回0,失败返回-1 - 命令查看共享内存:
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信号:软中断
信号概述
- 信号的名字和编号:以SIG开头,定义在
signal.h
头文件中,信号名都定义为正整数,具体的信号名称可以使用kill -l
来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号,kill对于0信号有特殊的应用
- 信号的处理:忽略、捕捉和默认动作
- 忽略信号,大多数信号可以使用这个方式来处理,担忧有两种信号不能被忽略:SIGKILL和SIGSTOP,如果忽略了,那么这个进程就变成了没人能管理的进程
- 捕捉信号(主要使用这一点,来实现异步通讯):写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理
- 系统默认动作,对于每个信号来说,系统都对应有默认的处理动作,当发送了该信号,系统会自动执行。具体的信号默认动作可以使用
man 7 signal
来查看系统的具体定义,或《UNIX环境高级系统编程》
- 信号处理函数的注册
- 入门:signal()
- 高级:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
也就是能处理消息
- 信号处理发送函数
- 入门:kill()
- 高级:sigqueue(),也就是能携带消息
- 简单例子——捕捉信号:
#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;
}
信号如何携带消息
- 发信号
- 用什么发
- 怎么放入消息
- 收信号
- 用什么绑定函数:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
:- 第一个参数为信号的编号
- act是一个指向
sigaction
结构体的指针类型参数,用于设置新的信号处理函数,如果act
指针不为空,则会将act
中定义的新信号处理函数注册到signum
对应的信号上;否则表示此次调用只是查询或者修改现有的信号处理函数,而不需要更改sigaction
结构体中的内容 oldact
也是一个指向sigaction
结构体的指针类型参数,用于存储之前的信号处理函数,即在调用sigaction
函数时旧的信号处理函数会被复制到oldact
指向的地址中。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
更为灵活。该函数的三个参数解析如下:- 第一个参数表示信号的编号
- 第二个参数是一个指针,提供了有关发出信号的更多信息,如信号来源等,传递给信号处理函数的数据,以及其他的一些信息。
- 第三个参数包含一些额外的数据,这个参数不会被内核修改,而且只能由发送者在用户空间中设置。如描述段错误原因的信息、内存地址或其他上下文信息等。
sa_mask
成员变量是信号掩码,用于设置在执行信号处理函数期间需要被屏蔽的信号集合。在执行信号处理函数期间,系统将不会响应被屏蔽的信号。sa_flags
成员变量是用于设置一些标志位的,例如SA_RESTART
指定了当系统调用被中断时自动重新启动这个系统调用,SA_NOCLDSTOP
表示在子进程停止或继续运行时不会通知父进程等等。sa_restorer
成员变量是一个指向函数的指针类型,它用于恢复在信号处理函数中被中断的系统调用。在有些情况下,在执行完信号处理函数之后,需要恢复被中断的系统调用,以使程序正常执行。
- 如何读出消息
int sigqueue(pid_t pid, int sig, const union sigval value);
- 第一个参数为:发给谁
- 发送什么信号
- 数据
- 示例代码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操作:放回锁
相关函数
int semget(key_t key,int num_sems, int sem_flags);
:创建或获取一个信号量组,若成功则返回信号量集ID,失败返回-1int semop(int semid,struct sembuf semoparrayp[],size_t numops);
:对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1int 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;
}