简介:这篇文章偏基础,适合初识IPC,里面有几种通信方法的详细函数介绍,每种通信方式通过代码实例解析能更好的理解每种通信方式,文章有点长,还请耐心阅读,一定会对大家有帮助。
关键字:IPC、管道、消息队列、共享内存、信号、信号量(我们下次讲,信号量和我们的其他几种功能不同)、单工、双工。
1.进程间通信简介(IPC)
1.1 简介
什么是IPC呢?为什么会出现IPC呢?这其实就是顺理成章的事情。拿企业举个例子,企业中某一个车间负责整车的某一个部分,这个车间就好比如一个进程,根据任务的不同就有了不同的车间(不同功能的进程)。这些车间总要有沟通吧,不同车间生产的东西跟我们车间生产的东西配合是否正常,这就出现了一些沟通方式。
关于进程的基本知识在之前一篇文章中讲到过,想了解的可以看看
Linux应用开发基础 进程篇
1.2 通信方式
1.管道:无名管道、有名管道。(ps aux | grep “bash”)中间的“|”就相当于一个管道,半双工。
2.消息队列:内核链表,半双工。
3. 信号:系统开销小,单工。
4. 共享内存:全双工。
5. 内存映射:全双工。
6. 套接字:全双工。
1.3 单工、双工
1.3.1 单工
数据传输是单向的。通信双方中,一方固定为发送端,另一方则固定为接收端,信息只能沿一个方向传输。
1.3.2 双工
1.半双工:
数据可以沿两个方向传送,但在同一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信。
2.全双工:
数据通信允许数据同时在两个方向上传输,相当于两个单工通信方式的结合。它要求发送设备和接收设备都有独立的接收和发送能力。
2. 通信方式
2.1 管道
在内核缓冲区,不占用磁盘,数据只能读一次不能保存。
2.1.1 无名管道
2.1.1.1特点:
只能通过fork创建通讯。
大小为4096,通过ulimit -a 或者 fpathconf函数查看。
环形队列(FIFO)。
读端和写端对应两个进程。
进程退出管道自动销毁。
管道默认是阻塞的。
2.1.1.2 创建
int pipe(int fd[2])
/*
fd[0]: 读端文件描述符;
fd[1]: 写端文件描述符;
return: 成功0,错误-1。
2.1.1.2.1 实例
无名管道实现 ps aux | grep bash
插个函数介绍
int dup2(int oldfd, int newfd);
/*
文件描述符重定向
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int pfd;
pid_t pid;
char buf[128]= {0};
char readbuf[128] = {0};
int fd[2];
pfd = pipe(fd);
if(pfd == -1)
{
perror("pipe");
exit(1);
}
long size = fpathconf(fd[0], _PC_PIPE_BUF); //返回fd[0]管道缓冲长度的限制值
printf("the length of pipe buffer is %ld\n", size);
printf("fd[0]: %d, fd[1]: %d\n", fd[0], fd[1]);
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
if(pid == 0)
{
close(fd[0]);
dup2(fd[1],STDOUT_FILENO); //重定向标准输出到fd[1]
system("ps ajx");
close(fd[1]);
}
else if(pid>0)
{
close(fd[1]);
dup2(fd[0],STDIN_FILENO); //重定向标准输入到fd[0]
system("grep bash");
close(fd[0]);
}
return 0;
}
2.1.1.3 管道读写行为
2.1.1.3.1 读操作
1.有数据
read(fd[1]) 正常读,返回读出的字节数
2.无数据
写端被全部关闭,read返回0,相当于读文件到了尾部
没有全部关闭,read阻塞
2.1.1.3.2 写操作
1.读端全部关闭
管道破裂,进程被终止
内核发送SIGPIPE,默认处理动作
2.读端没有全部关闭
缓冲满了,write阻塞
缓冲没满,继续写,直到写满阻塞。
2.1.2 有名管道
无名管道只能亲缘进程间通讯,所以就出现了有名管道。
特点:
管道中有一个伪文件,大小为0 ls -l
其他和无名管道一样
就是通过这样一个伪文件来通信。
2.1.2.1 创建
int mkfifo(const char filename, mode_t mode);
/*
filename: 伪问文件名
mode: 文件访问权限
return: 成功0 失败-1
2.1.2.2 实例
同样实现ps ajx | grep bash
2.1.2.2.1 pipe_write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char** argv)
{
if(argc != 3)
{
printf("parameter amount anomaly\n");
exit(1);
}
int fiforet;
int pfd;
fiforet = mkfifo("mkfifo", 0777);
pfd = open("mkfifo", O_RDWR);
if(pfd<0)
{
perror("mkfifo");
exit(1);
}
dup2(pfd,STDOUT_FILENO);
close(pfd);
execlp(argv[1], argv[1], argv[2],NULL); //execl函数族,相当于一个新进程,具体的在开头推荐的进程篇中有详细讲解
return 0;
}
2.1.2.2.2 pipe_read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char** argv)
{
if(argc != 3)
{
printf("parameter amount anomaly\n");
exit(1);
}
int fiforet;
int pfd;
pfd = open("mkfifo", O_RDWR);
if(pfd<0)
{
perror("mkfifo");
exit(1);
}
dup2(pfd, STDIN_FILENO);
execlp(argv[1], argv[1], argv[2],NULL);
return 0;
}
这里出现了一个问题,终端输出之后,read读取没有退出,grep bash 一直在处于S+状态,搞了好久没解决,如果有知道的请解释一下,如何解决。
2.2 消息队列
消息对列是内核的链表,存放在内核中。
ipcs 查看各种关于IPC相关的信息
2.2.1 特点
1.消息是面向记录的,其中消息具有特定的格式和优先级。
2.消息队列独立于发送和接收进程,进程终止,消息队列仍然存在。
3.消息队列可以实现随机查询,不一定要按照先进先出的顺序,可以按照消息类型读取。
2.2.2 函数介绍
2.2.2.1 msgget函数
int msgget(key_t key, int msgflg);
//创建或打开消息队列,
参数:
key:和消息队列关联的key值
msgflg:是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或
操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被
忽略,而只返回一个标识符。
返回值:成功返回队列ID,失败则返回‐1,
2.2.2.2 msgsnd函数
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
2.2.2.3 msgrcv函数
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
2.2.2.4 msgctl函数
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
2.2.2.5 ftok函数
key_t ftok( char * fname, int id )
//系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
参数:
fname就时你指定的文件名(该文件必须是存在而且可以访问的)。
id是子序号, 虽然为int,但是只有8个比特被使用(0‐255)。
返回值:
当成功执行的时候,一个key_t值将会被返回,否则 ‐1 被返回。
2.2.3 实例
2.2.3.1 msg_server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
typedef struct msq{
long mtype;
char mtext[128];
} msg;
int main()
{
msg sendmsg, recvmsg;
key_t key;
int msgid;
pid_t pid;
//1.build IPC
key = ftok("a.c", 2); //获取IPC通信key值
if(key==-1)
{
perror("ftok");
exit(1);
}
printf("the key is %d\n", key);
//2.build msg_queue
msgid = msgget(key, IPC_CREAT|0755);
if(msgid==-1)
{
perror("msgget");
exit(1);
}
system("ipcs -q");
//3.fork
pid = fork();
if(pid==-1)
{
perror("fork");
exit(1);
}
sendmsg.mtype = 200; //消息类型200,server读,client写
//3.1.child process
if(pid==0)
{
//msgrcv
while(1)
{
memset(recvmsg.mtext,0,128);
if(msgrcv(msgid, (void*)&recvmsg, 128, 100, 0) == -1) //server从消息类型100中读
{
perror("msgrcv");
exit(1);
}
printf("THE SERVER RECEIVES SUCCESSFULLY, %s\n", recvmsg.mtext);
}
}
//3.2.father process
else if(pid>0)
{
while(1)
{
memset(sendmsg.mtext, 0, 128);
//msgsnd
printf("PLEASE INPUT YOUR MESSAGE:\n");
if(fgets(sendmsg.mtext, 128, stdin) == NULL)
{
printf("the msg-text fails to require from stdin\n");
exit(1);
}
if(msgsnd(msgid, (void*)&sendmsg, strlen(sendmsg.mtext), 0)==-1)
{
perror("msgsnd");
exit(1);
}
}
}
return 0;
}
2.2.3.2 msg_client.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
typedef struct msq{
long mtype;
char mtext[128];
} msg;
int main()
{
msg sendmsg, recvmsg;
key_t key;
int msgid;
pid_t pid;
//1.build IPC
key = ftok("a.c", 2);
if(key==-1)
{
perror("ftok");
exit(1);
}
printf("the key is %d\n", key);
//2.build msg_queue
msgid = msgget(key, IPC_CREAT|0755);
if(msgid==-1)
{
perror("msgget");
exit(1);
}
system("ipcs -q");
//3.fork
pid = fork();
if(pid==-1)
{
perror("fork");
exit(1);
}
sendmsg.mtype = 100; //消息类型100,client写,server读
//3.1.child process
if(pid==0)
{
//msgrcv
while(1)
{
memset(recvmsg.mtext,0,128);
if(msgrcv(msgid, (void*)&recvmsg, 128, 200, 0) == -1) //client从200中读
{
printf("cuole\n");
perror("msgrcv");
exit(1);
}
printf("THE SERVER RECEIVES SUCCESSFULLY, %s\n", recvmsg.mtext);
}
}
//3.2.father process
else if(pid>0)
{
while(1)
{
memset(sendmsg.mtext,0,128);
//memset(sendmsg.mtext, 0, 128);
//msgsnd
printf("PLEASE INPUT YOUR MESSAGE:\n");
if(fgets(sendmsg.mtext, 128, stdin) == NULL)
{
printf("the msg-text fails to require from stdin\n");
exit(1);
}
if(msgsnd(msgid, (void*)&sendmsg, strlen(sendmsg.mtext), 0)==-1)
{
perror("msgsnd");
exit(1);
}
}
}
return 0;
}
终端输出
2.3 共享内存
2.3.1 特点
1.共享内存(shared memory)是进程间共享和交换数据最快捷的方式,无需通过系统调用与内核交互,但是缺点很明显,多个进程同时操作一个数数据,如果不通过有效的管理,会发生数据紊乱,问题很严重。
2.消息队列的的数据能被多次读取。
2.3.2 函数介绍
2.3.2.1 shmget函数
1.int shmget(key_t key, size_t size, int shmflg);
//用来获取或创建共享内存
参数:
key:IPC_PRIVATE 或 ftok的返回值
size:共享内存区大小
shmflg:同open函数的权限位,也可以用8进制表示法
返回值:
成功:共享内存段标识符‐‐‐ID‐‐‐文件描述符
出错:‐1
2.3.2.2 shmat函数
2.void *shmat(int shm_id, const void *shm_addr, int shmflg);
//把共享内存连接映射到当前进程的地址空间
参数:
shm_id:ID号
shm_addr:映射到的地址,NULL为系统自动完成的映射
shmflg:
SHM_RDONLY共享内存只读
默认是0,表示共享内存可读写
返回值:
成功:映射后的地址
失败:NULL
2.3.2.3 shmdt函数
3.int shmdt(const void *shmaddr);
//将进程里的地址映射删除
参数:
shmid:要操作的共享内存标识符
返回值:
成功:0
出错:‐1
2.3.2.4 shmctl函数
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
2.3.3 实例
2.3.3.1 shm_wirte.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
int shmid;
key_t key;
char *shmp;
key = ftok("a.c", 1);
if(key ==-1)
{
perror("ftok");
exit(1);
}
printf("the key is %d\n", key);
shmid = shmget(key,128,IPC_CREAT|0777);
if(shmid==-1)
{
perror("shmget");
exit(1);
}
printf("shmid:%d\n", shmid);
shmp = (char*)shmat(shmid, NULL, 0);
while(1)
{
printf("PLEASE INPUT\n");
fgets(shmp, 128, stdin);
if(!strcmp(shmp, "exit\n")) break;
}
shmdt(shmp);
printf("write port has exited\n");
return 0;
}
2.3.3.2 shm_read.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
key_t key;
int shmid;
char *shmp;
key = ftok("a.c", 1);
if(key<0)
{
perror("ftok");
exit(1);
}
printf("the key is %d\n", key);
shmid = shmget(key, 128, IPC_CREAT|0777);
if(shmid<0)
{
perror("shmget");
exit(1);
}
printf("shmid:%d\n", shmid);
shmp = (char*)shmat(shmid, NULL, 0);
while(1)
{
if(strlen(shmp)==0) continue;
if(!strcmp(shmp,"exit\n")) break;
printf("the shmp content is %s, length: %ld\n", shmp, strlen(shmp));
memset(shmp,0,128);
}
shmdt(shmp);
shmctl(shmid, IPC_RMID,NULL);
printf("read port has exited\n");
return 0;
}
终端输出
这里存在一个问题,为什么输入的长度比输出大1,因为fgets函数的性质,fgets函数将回车符也写入到了输入数据中,所以才会如此。
这里可能会下面这种功能上的bug,大家想想当一个共享内存有多个进程进行读写操作的时候,这些进程没有进行先后顺序的管理会发生什么,下面这种就是例子,写端还没有完全写进内存,数据就已经开始读了,这中情况不是我们想要的结果,所以我们要限制他们,写完之后才能重新读,读完之后才能重新写。
这篇文章会写解决方案:
Linux信号量PV操作
2.4 信号
2.4.1 特点
1.只能由内核发送信号,用户程序不能发送信号,用户程序可以使用一些函数通过告知内核发送什么信号。
2.小信息,系统开销小,单工。
通过 kill -l 查看内核能够发送多少种信号
2.4.2 函数介绍
信号的发送(发送信号进程):kill、raise、alarm
信号的处理(自定义信号处理方式) :signal
SIGKILL, SIGSTOP这两个信号进程捕获不到,这是强制执行的。
2.4.2.1 kill函数
#include<signal.h>
#include<sys/types.h>
函数原型:int kill(pid_t pid, int sig);
参数:
函数传入值:pid
正数:要接收信号的进程的进程号
0:信号被发送到所有和pid进程在同一个进程组的进程 ‐1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
sig:信号
函数返回值:成功 0 出错 ‐1
2.4.2.1.1 实例
2.4.2.1.1.1 kill.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc, char **argv)
{
if(argc!=3)
{
printf("argument amount anomaly\n");
exit(1);
}
int pid = atoi(argv[2]);
int sig = atoi(argv[1]);
int ret = kill(pid, sig);
if(ret == -1)
{
perror("kill");
exit(1);
}
printf("the %d process is killed\n", pid);
return 0;
}
2.4.2.1.1.2 demo.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("the pid of process is %d\n",getpid());
pause();
return 0;
}
终端输出
demo.c中使用了pause()函数但是进程状态不是字面意思上的暂停状态,而是睡眠状态。
2.4.2.2 raise函数
相当于kill(getpid(),signame) 给自己发送信号。
所需头文件:
#include<signal.h>
#include<sys/types.h>
函数原型:
int raise(int sig);
参数:
函数传入值:sig:信号
函数返回值:
成功 0 出错 ‐1
2.4.2.2.1 实例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
printf("the pid of process is %d\n",getpid());
raise(SIGKILL);
while(1);
return 0;
}
终端输出
2.4.2.3 alarm函数
所需头文件#include <unistd.h>
函数原型 unsigned int alarm(unsigned int seconds)
参数:
返回值:
出错:‐1
seconds:指定秒数
成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则 返回上一个闹钟时间的剩余时间,否则返回0。
2.4.2.3.1 实例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
printf("the pid of process is %d\n",getpid());
alarm(5);
for(int i=0;i<10;i++)
{
printf("-------%d-------\n",i);
sleep(1);
}
return 0;
}
终端输出
2.4.2.4 signal函数
所需头文件 #include <signal.h>
函数原型 void (*signal(int signum, void (*handler)(int)))(int);
函数传入值
signum:指定信号
handler
SIG_IGN:忽略该信号。
SIG_DFL:采用系统默认方式处理信号
自定义的信号处理函数指针
函数返回值
成功:设置之前的信号处理方式
出错:‐1
signal 函数有二个参数,第一个参数是一个整形变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函
数;这个函数的返回值是一个函数指针。
2.4.2.4.1 实例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void func(int signum)
{
printf("hello world, signum:%d\n",signum);
}
int main()
{
pid_t pid;
pid = fork();
signal(SIGCHLD,func);
if(pid<0)
{
perror("fork");
exit(1);
}
if(pid==0)
{
int i = 0;
while(i<5)
{
sleep(1);
printf("-----%d------\n",i++);
}
}
else if(pid>0)
{
printf("the child pid:%d, the father pid:%d\n",pid,getpid());
wait(NULL);
}
}
终端输出
3.总结
这篇文章是对进程间常见的几种通信方式的介绍,管道、消息队列、共享内存、信号、信号量,信号量会在另一篇文章中通过解决共享内存出现的bug让大家对信号量能有更深刻的认识,文章写太长,容易感到枯燥,希望这些知识对大家有帮助。
文章如有错误请各位指出,谢谢。