一.为什么要进程通信
首先明白,进程是一个独立的资源分配单位==>进程间相互独立,没有关联,即进程无法直接访问另一个进程。但是进程间总会在各种场合需要进行通信,例如服务端和客户端需要无时无刻的通信
二.进程间通信常见的四种目的
1.数据传输:一个进程需要将它的数据发送给另一个进程。
2.资源共享:多个进程之间共享同样的资源。
3.通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
4.进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
三.进程通信是在内核中实现的
提出疑问:如何解决进程间通信?----------->全局变量
四.进程间通信方式(这里转载进程之间为什么需要通信?_spring to do的博客-CSDN博客_进程之间为什么要通信)
声明:在学习通信学习思路,本人是基于文件IO的思想
(1)无名管道
队列的数据结构FIFO(first in first out)
函数原型int pipe(int fd[2]) /*创建管道*/
参数:fd[0]:表示读端,fd[1]:表示写端
函数传入值fd[2]:管道的两个文件描述符,之后就是可以直接操作者两个文件描述符
返回值 成功0 失败-1
int main(int argc, char *argv[])
{
pid_t pid; //子进程
char process_inter = 0; //用一个变量,放到管道里,来实现进程之间的通信
int ret, i; //pipe 的返回值
int fd[2]; //管道的两端
ret = pipe(fd);
if (ret < 0)
{
printf("create pipe failed\n");
return -1;
}
printf("create pipe success\n");
pid = fork();
if (pid == 0)
{
//如果父进程没有写入,会在这儿卡着,打印不错后面 进程状态为sleep
read(fd[0], &process_inter, 1);
while (process_inter == 0);
for (i = 0; i < 5; i++)
{
printf("I am Child Process\n");
}
}
if (pid > 0)
{
for (i = 0; i < 5; i++)
{
printf("I am Parent Process\n");
}
sleep(3);
process_inter = 1;
write(fd[1], &process_inter, 1);
}
return 0;
}
缺点:不能实现不是父子进程(亲属关系)之间通信
(2)有名管道=》补充无名管道缺点
因为每一个文件节点都有一个inode号,而且新的文件类型:管道文件:‘ P’表示管道类型,用mkfifo---这个是不占内存的
int mkfifo(const char* filename,mode_t mode)
参数一:管道文件名
参数二:权限(注意:实际权限与umask有关==> mode&(~umask))
(3)信号通信----这个自己也还不是很懂
1. kill -l :查看有多少种信号
2.信号通信其实就是内核向用户空间进程发送信号,只有内核才能发送信号,用户空间进程不能发送信号。信号在内核中有很多种信号
3.实现通信过程
例:①A告诉内核发送什么信号 ②告诉内核发给谁(PID号)
函数原型:int kill(pid_t pid, int sig);
函数传入值:pid(进程号)
正数:要接收信号的进程的进程号
0:信号被发送到所有和pid进程在同一个进程组的进程
-1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
sig:信号
函数返回值:成功:0;失败:-1
4.信号通信的框架
信号的发送(发送信号进程):kill raise alarm
信号的接收(接收信号进程) : pause() , sleep(), while(1)
信号的处理(接收信号进程) : signal
①int raise(int sig) /*发送给自己*/
即raise() = kill( getid() , sig );
②alarm():只会发送SIGALARM信号,发送闹钟信号 ==> 会让内核定时一段时间之后发送信号,和raise一样,都是让内核发送信号给当前信号,不同的是raise()是立即
函数原型:unsigned int alarm(unsigned int seconds)
函数传入值:seconds:指定秒数
函数返回值:成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
失败:-1.
信号的接收:前提条件:进程不能结束
①pause():进程状态S(睡眠)
信号处理:有默认的处理方式:忽略,终止,暂止
signal:自己处理信号的方式告诉内核,这样进程收到信号就可以采用自己的处理方式
void (*signal(int signum, void (*handler)(int)))(int);
signum:指定信号
handler:SIG_IGN:忽略该信号
SIG_DEF:采用默认方式处理信号
自定义的信号处理函数指针
返回:成功:设置之前的处理技术
失败:-1
解破函数:
A=void (*handler)() == 函数指针变量,形式:含有一个整形参数,无返回值的函数
void (*signal( int signum , A(int) ));
signal: 有两个参数 :一参:信号值 , 二参:函数指针
例子:父子进程发送信号
void myfunc(int signum)
{
int i;
i = 0;
while (i < 2)
{
printf("process things signum %d\n", signum); //10
sleep(1);
i++;
}
return; //返回 main 函数
}
void myfunc1(int signum)
{
printf("receive %d\n", signum);
wait(NULL);
return; //返回 main 函数
}
void myfunc2(int signum)
{
printf("2receive %d\n", signum);
wait(NULL);
return; //返回 main 函数
}
int main()
{
int status = 0;
pid_t pid;
pid = fork(); //while((p1=fork( ))==-1); /*创建子进程p1*/
if (pid > 0)
{
//wait(NULL);如果想在这里回收子进程会产生阻塞
if (waitpid(pid, &status, WNOHANG) == 0) //成功返回子进程的pid
{
printf("收到exit status%d\n", status); //收到子进程的pid
printf("收到exit status %d\n", pid);
}
signal(10, myfunc); //14就是alarm信号
// signal(20, myfunc1); //17就是exit里面的child信号
// signal(SIGCHLD, myfunc2); //17就是exit里面的child信号
while (1)
{
sleep(1);
printf("I am father\n");
}
}
if (pid == 0)
{
// pause();
while (1)
;
// kill(getppid(), 10); //用户自定义信号 10
// sleep(5);
// exit(0); //里面包括了发送给父进程的kill(getppid(), SIG_CHLD) 17
}
return 0;
}
第三类:IPC通信---(Inter-Process Communicate)
1.同样实在内核空间,和文件I/O类似,但是函数形式改变了
2.IPC对象在内核种原来是不存在的,需要在用户层完成创建,删除
3.分类:
(1)共享内存
(2)信号灯
(3)信号队列
共享内存
(1). 共享内存的概念(转载:共享内存原理及实现通信过程_几亿少女的梦的博客-CSDN博客_共享内存实现原理)
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
进程通信的本质就是让两个进程看到同一个空间,所以共享内存就是在物理内存中申请一块空间,然后让通信的每个进程通过进程地址空间与页表完成从物理地址到进程地址空间的映射,然后就可以对该块空间进行修改与读取了。
(2). 共享内存的创建过程(转载共享内存原理及实现通信过程_几亿少女的梦的博客-CSDN博客_共享内存实现原理)
①.首先要申请共享内存,也就是在物理地址中开辟好共享内存空间,供进程间通信使用。而且我们知道物理地址中的共享内存可能会有很多个,那么肯定就要管理这些内存,所以要管理我们就要先描述再组织,所以出了创建共享内存之外,还要为共享内存创建描述共享内存的内核数据结构。
②.将共享内存挂接到地址空间,本质上就是给两个进程之间的共享内存和虚拟地址空间之间建立映射,之后就可以开始进程间的数据传输与修改了。
③.用完之后要去掉共享内存和进程地址空间之间的联系,本质上就是修改页表,取消共享内存和虚拟内存的映射关系。
释放共享内存,将内存归还给系统。
(3)创建函数:
int shmget(key_t key , size_t size , int shmflag)
一参:IPC_PRIVATE(一般用于亲缘进程间通信)或者ftok()的返回值-------用来标识共享内存,可以保证共享内存本身的唯一性
char ftok(const char* path , char key)
一参:文件路径,文件名
二参:一个字符
创建函数二参:共享内存大小
三参:权限位,可以用8进制表示法
(4)
查看IPC对象
ipc -m(共享内存) -q(共享队列) -s(信号灯)
删除IPC对象
ipcrm -m id
(5)shmat函数原型
(6)用完共享内存要释放,映射也需要释放
int shmdt(const void* shmaddr)-------------->将进程地址映射删除
参:shmaddr共享内存映射后地址
成功返回0,失败返回-1
int shmctl(int shmid , int cmd ,struct shmid ds *buf)
举个例子 🌰
仍然是父子间通信,父进程一直写入,子进程一直读取:
void myfun(int signum)
{
return;
}
int main(int argc, char *argv[])
{
pid_t pid;
int shmid; //创建一个内核共享内存,让父进程写,子进程读
char *p;
//必须先写到创建 子进程 之前,这样才能共享的是同一块共享内存,否则是各自操作各自的。
shmid = shmget(IPC_PRIVATE, 128, IPC_CREAT | 0777);
if (shmid < 0)
{
printf("create share memory failed\n");
return -1;
}
printf("create share memory ok. shmid=%d\n", shmid);
pid = fork();
if (pid < 0)
{
printf("create new process failed\n");
return -2;
}
if (pid > 0)
{
signal(SIGUSR2, myfun); //父进程接收子进程读完信号
p = (char *)shmat(shmid, NULL, 0); //自动分配内存,可读写
if (NULL == p)
{
printf("parent process:shmat function failed\n");
return -3;
}
while (1)
{
printf("parent process write start write in this shared memory:\n");
// scanf("%d", p);
fgets(p, 128, stdin); //标准输入
kill(pid, SIGUSR1); //父进程发送给子进程写完信号
pause(); //等待子进程读
}
}
if (0 == pid)
{
signal(SIGUSR1, myfun); //子进程接收父进程写完信号
p = (char *)shmat(shmid, NULL, 0); //自动分配内存,可读写
if (NULL == p)
{
printf("child process:shmat function failed\n");
return -3;
}
while (1)
{
pause(); //等待父进程写
printf("share memory data:%s", p);
kill(getppid(), SIGUSR2); //子进程发送读完信号
}
}
shmdt(p); //删除映射出来的共享内存
shmctl(shmid, IPC_RMID, NULL);
//或者写
// system("ipcrm -m shmid");
system("ipcs -m");
return 0;
}
消息队列
(1)创建或打开消息队列
int msgget(key_t key , int flag)
key:和消息队列关联的key值
flag:访问权限
(2)msgclt:删除消息队列
int msgclt ( int msgid , int cmd , struct msgid_ds *buf)
msgid:消息队列ID
cmd{ IPC_STAT == ipc -q(查看消息队列) , IPC_SET(设置属性) , IPC_RMID(删除) }
buf
struct msgbuf {
long msgtype; /* 消息类型,必须 > 0 */
char buf[BUF_SIZE]; /* 消息文本 */
};
(3)写:msgsnd-------队列相当于插入
int msgsnd ( int msqid, struct msgbuf *msgp, int msgsz, int msgflg )
第一个参数msqid 是消息队列对象的标识符(由msgget()函数得
到)
第二个参数msgp 指向要发送的消息所在的内存
第三个参数msgsz 是要发送信息的长度(字节数),可以用以下的公式计算:
msgsz = sizeof(struct mymsgbuf) - sizeof(long);
第四个参数是控制函数行为的标志,可以取以下的值:
0,忽略标志位;
IPC_NOWAIT,如果消息队列已满,消息将不被写入队列,控制权返回调用函数的线
程。如果不指定这个参数,线程将被阻塞直到消息被可以被写入。
例子:两个非亲缘线程单向通信,双向通信,类似于聊天工具
server.c
#include "sys/types.h"
#include "sys/msg.h"
#include "signal.h"
#include "unistd.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
struct msgbuf
{
long type;
char voltage[124];
char ID[4];
};
int main()
{
int msgid;
int readret;
int key;
int pid;
struct msgbuf sendbuf,recvbuf;
key=ftok("./a.c",'a');
if(key < 0)
{
printf("creat key failure\n");
return -2;
}
msgid=msgget(key,IPC_CREAT|0777);
if(msgid <0)
{
printf("creat message queue failure\n");
return -1;
}
printf("creat message queue sucess msgid=%d\n",msgid);
system("ipcs -q");
pid=fork();
if(pid > 0) //parent process write 100
{
sendbuf.type=100;
//write message queue
while(1)
{
memset(sendbuf.voltage,0,124);//clear send buffer
printf("please input message:\n");
fgets(sendbuf.voltage,124,stdin);
//start write message to message queue
msgsnd(msgid,(void *)&sendbuf,strlen(sendbuf.voltage),0);
}
}
if(pid == 0)//child process read 200
{
while(1)
{
memset(recvbuf.voltage,0,124);
msgrcv(msgid,(void *)&recvbuf,124,200,0);
printf("receive message from message queue:%s",recvbuf.voltage);
}
}
msgctl(msgid,IPC_RMID,NULL);
system("ipcs -q");
return 0;
}
client.c
#include "sys/types.h"
#include "sys/msg.h"
#include "signal.h"
#include "unistd.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
struct msgbuf
{
long type;
char voltage[124];
char ID[4];
};
int main()
{
int msgid;
int readret;
int key;
int pid;
struct msgbuf sendbuf,recvbuf;
key=ftok("./a.c",'a');
if(key < 0)
{
printf("creat key failure\n");
return -2;
}
msgid=msgget(key,IPC_CREAT|0777);
if(msgid <0)
{
printf("creat message queue failure\n");
return -1;
}
printf("creat message queue sucess msgid=%d\n",msgid);
system("ipcs -q");
pid=fork();
if(pid == 0) //child process write 200
{
sendbuf.type=200;
//write message queue
while(1)
{
memset(sendbuf.voltage,0,124);//clear send buffer
printf("please input message:\n");
fgets(sendbuf.voltage,124,stdin);
//start write message to message queue
msgsnd(msgid,(void *)&sendbuf,strlen(sendbuf.voltage),0);
}
}
if(pid > 0)//parent process read 100
{
while(1)
{
memset(recvbuf.voltage,0,124);
msgrcv(msgid,(void *)&recvbuf,124,100,0);
printf("receive message from message queue:%s",recvbuf.voltage);
}
}
msgctl(msgid,IPC_RMID,NULL);
system("ipcs -q");
return 0;
}
信号灯
学习目的:一个是用于多个共享资源的互斥使用,另一个是用与并发线程数的控制
信号灯是信号量的集合
(1)int semget(key_t key, int nsems, int semflg);
key:所创建或打开信号量集的键值。
nsems:创建的信号量集中的信号量的个数,该参数只在创建信号量集时有效。
semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过or表示
(2)int semctl(int _semid ,int _semnum,int _cmd ……);
功能:控制信号量的信息。
返回值:成功返回0,失败返回-1;
参数:
_semid 信号量的标志码(ID),也就是semget()函数的返回值;
_semnum, 操作信号在信号集中的编号。从0开始。
_cmd 命令,表示要进行的操作。
参数cmd中可以使用的命令如下:
IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID将信号量集从内存中删除。
GETALL用于读取信号量集中的所有信号量的值。
GETNCNT返回正在等待资源的进程数目。
GETPID返回最后一个执行semop操作的进程的PID。
GETVAL返回信号量集中的一个单个的信号量的值。
GETZCNT返回这在等待完全空闲的资源的进程数目。
SETALL设置信号量集中的所有的信号量的值。
SETVAL设置信号量集中的一个单独的信号量的值。
信号量(posit)的操作步骤
(1)定义一个信号量-->sem_t sem
(2)初始化信号量-->seminit()
p操作或wait操作--->sem_wait ,sempost
(3)int semop(int semid ,struct sembuf *_sops ,size_t _nsops);
功能:用户改变信号量的值。也就是使用资源还是释放资源使用权。
参数:
_semid : 信号量的标识码。也就是semget()的返回值。
_sops是一个指向结构体数组的指针。
struct sembuf{
unsigned short sem_num;//第几个信号量,第一个信号量为0;
/*
如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。
*/
short sem_op;//对该信号量的操作。
short _semflg;
};
nsops:操作信号灯的数量,恒大于或等于1。
例子---易懂
#include "sys/sem.h"
#include "signal.h"
#include "unistd.h"
#include <sys/types.h>
#include <sys/ipc.h>
int main()
{
int semid;
semid=semget(IPC_PRIVATE,3,0777);//建立三个信号
if(semid <0)
{
printf("creat semaphore failure\n");
return -1;
}
printf("creat semaphore sucess semid=%d\n",semid);
system("ipcs -s");
//while(1);
// delete semaphore
//删除信号灯里面的第一个(0), 第四个参数可以NULL也可以不写
semctl(semid,0,IPC_RMID,NULL);
system("ipcs -s");
return 0;
}