1、进程间通信方式
进程间通信的本质是读写操作。创建进程后会自动分配4G虚拟内存,其中1G内核空间和3G用户空间。
每个进程的用户空间时私有的,但是内核空间时当前主机中所有进程公有的,所以如果进程间要共享资源,需要对同一块内核空间进行操作。
共享内存:是所有进程通信方式中效率最高的,共享内存是直接对物理内存进行操作。
套接字通信方式可以实现不同主机之间进行通信。
1、管道
1、无名管道
创建无名管道
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个无名管道
参数:
pipefd:数组,用户保存对无名管道读写的文件描述符
pipefd[0] 用于从管道读取数据
pipefd[1] 用于向管道写入数据
返回值:
成功:0
失败:-1
管道可以看做是一种特殊的文件,对于它的读写是使用的文件IO如read和write。
无名管道只能用于具有亲缘关系的进程之间的通信,为半双工的通信模式。
具有固定的读端和写端。当一个管道建立时,他会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。是在进程的内核空间里开辟的区域并得到两个文件描述符。
例如:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//使用pipe函数创建无名管道
//无名管道是在内核空间开辟的区域,通过两个文件描述符对当前区域进行读写操作
int pipefd[2];
if(pipe(pipefd) == -1)
{
perror("fail to pipe");
exit(1);
}
//printf("pipefd[0] = %d\n", pipefd[0]);
//printf("pipefd[1] = %d\n", pipefd[1]);
//对管道的操作使用read和write函数
//无名管道写入数据时,第二次写入的数据不会覆盖之前的,会在之前的数据后面追加写入
//无名管道读取数据时,如果管道中有数据,会读取成功,如果管道中没有数据,会阻塞等待
//从无名管道中读取数据后,读取的数据会从管道中清除
//向管道写入数据,使用pipefd[1]
if(write(pipefd[1], "hello world", 11) == -1)
{
perror("fail to write");
exit(1);
}
if(write(pipefd[1], "666666", 6) == -1)
{
perror("fail to write");
exit(1);
}
//注意:无名管道无法修改偏移量
// if(lseek(pipefd[0], 8, SEEK_SET) == -1)
// {
// perror("fail to lseek");
// exit(1);
// }
//从管道中读取数据,使用pipefd[0]
char buf[128] = "";
if(read(pipefd[0], buf, sizeof(buf)) == -1)
{
perror("fail to read");
exit(1);
}
printf("buf = %s\n", buf);
if(read(pipefd[0], buf, sizeof(buf)) == -1)
{
perror("fail to read");
exit(1);
}
printf("buf = %s\n", buf);
return 0;
}
如果管道内没有数据,读端read函数会一直阻塞直到读到数据。
无名管道默认为64K空间,如果只写不读,管道写满write函数会阻塞。
使用无名管道进行进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//由于无名管道的标识是两个文件描述符
//所以只能在同一个程序中的多个进程间通信
//也就是具有亲缘关系的进程间实现通信
pid_t pid;
int pipefd[2];
if(pipe(pipefd) == -1)
{
perror("fail to pipe");
exit(1);
}
if((pid = fork()) < 0)
{
perror("fail to fork");
exit(1);
}
else if(pid > 0) // 父进程
{
//父进程负责给子进程发送数据
char buf[128] = {};
while(1)
{
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0';
if(write(pipefd[1], buf, sizeof(buf)) == -1)
{
perror("fail to write");
exit(1);
}
}
}
else //子进程
{
//子进程接收父进程的数据
char buf[128] = "";
while(1)
{
if(read(pipefd[0], buf, sizeof(buf)) == -1)
{
perror("fail to read");
exit(1);
}
}
}
return 0;
}
2、有名管道
创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:创建一个有名管道
参数:
pathname:有名管道的文件名
mode:有名管道的权限,一般0664
返回值:
成功:0
失败:-1
有名管道可以使互不相关的两个进程通信,可以通过路径表示,并且在文件系统中可见。
有名管道遵循先进先出规则,类似队列。有名管道也是在进程的内核空间区开辟区域,在本地创建一个文件来标识内核空间的区域。
例如:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建一个有名管道,使用mkfifo函数
if(mkfifo("myfifo", 0664) == -1)
{
//printf("errno = %d\n", errno);
//如果有名管道文件已经存在,没有必要打印错误信息,后期直接用就可以
if(errno != EEXIST)
{
perror("fail to mkfifo");
exit(1);
}
}
//有名管道的特点
//有名管道中写入数据,后写入的不会覆盖之前的,会以追加的方式写数据
//有名管道中读取数据,有数据可以正常读取,如果没有数据,read函数会阻塞
//读取到的数据会从管道中清除
//有名管道创建的文件只是起到标识作用,本质还是在内核空间李阿敏开辟区域,
//不同的进程可以通过这个文件标识找到相同的内核空间,这样就可以实现相互通信,
//所以有名管道创建的文件永远都是0个字节
//对有名管道进行操作
//使用文件io的函数对有名管道进行操作
int fd;
if((fd = open("myfifo", O_RDWR)) == -1)
{
perror("fail to open");
exit(1);
}
//使用write向有名管道写入数据
write(fd, "hello world", 11);
write(fd, "666666", 6);
//使用read从有名管道中读取数据
char buf[128] = "";
read(fd, buf, sizeof(buf));
printf("buf = %s\n", buf);
read(fd, buf, sizeof(buf));
printf("buf = %s\n", buf);
close(fd);
return 0;
}
有名管道实际操作类似文件的读写,多进程操作是类似多个进程对同一个文件进行读写,只不过需要加锁。
有名管道进程通信:
send.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
if(mkfifo("myfifo1", 0664) == -1)
{
if(errno != EEXIST)
{
perror("fail to mkfifo");
exit(1);
}
}
if(mkfifo("myfifo2", 0664) == -1)
{
if(errno != EEXIST)
{
perror("fail to mkfifo");
exit(1);
}
}
int fd_w, fd_r;
if((fd_w = open("myfifo1", O_WRONLY)) == -1)
{
perror("fail to open");
exit(1);
}
if((fd_r = open("myfifo2", O_RDONLY)) == -1)
{
perror("fail to open");
exit(1);
}
char buf[128] = "";
ssize_t bytes;
while(1)
{
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0';
if((bytes = write(fd_w, buf, sizeof(buf))) == -1)
{
perror("fail to write");
exit(1);
}
if((bytes = read(fd_r, buf, sizeof(buf))) == -1)
{
perror("fail to read");
exit(1);
}
printf("from recv: %s\n", buf);
}
return 0;
}
recv.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
if(mkfifo("myfifo1", 0664) == -1)
{
if(errno != EEXIST)
{
perror("fail to mkfifo");
exit(1);
}
}
if(mkfifo("myfifo2", 0664) == -1)
{
if(errno != EEXIST)
{
perror("fail to mkfifo");
exit(1);
}
}
int fd_w, fd_r;
if((fd_r = open("myfifo1", O_RDONLY)) == -1)
{
perror("fail to open");
exit(1);
}
if((fd_w = open("myfifo2", O_WRONLY)) == -1)
{
perror("fail to open");
exit(1);
}
char buf[128] = "";
ssize_t bytes;
while(1)
{
if((bytes = read(fd_r, buf, sizeof(buf))) == -1)
{
perror("fail to read");
exit(1);
}
printf("from send: %s\n", buf);
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0';
write(fd_w, buf, sizeof(buf));
}
return 0;
}
2、信号
在Linux系统中信号是由系统已经定义好的一些宏,所以不能自己定义,直接使用系统中的信号即可。使用kill -l可以查看系统定义的信号。
信号本质是软件中断,它是在软件层次上对中断机制的一种模拟。信号是一种异步通信方式。
信号是有当前系统已经定义好的一些表示,每一个标识都会在特定的场合使用,并且都会对进程有一定的影响,当信号产生时,会对当前进程作出相应的操作。
信号的相关操作:
Linux中信号内容博大精深,此处略微总结一种使用。
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
-->
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:当进程中产生某一个信号时,对当前信号进行处理
参数:
sig:指定要处理的信号
handler:处理方式
SIG_IGN 当信号产生时,以缺省(忽略)的方式处理
SIG_DFL 当信号产生时,以当前信号默认的方式处理
void handler(int sig):当信号产生时,通过信号处
理函数自定义方式处理,函数名可以随便写,
参数表示当前的信号
返回值:
成功:当前进程默认的处理方式
失败:SIG_ERR
例如:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void handler(int sig)
{
if(sig == SIGINT)
{
printf("SIGINT正在处理\n");
}
if(sig == SIGQUIT)
{
printf("SIGQUIT正在处理\n");
}
if(sig == SIGTSTP)
{
printf("SIGTSTP正在处理\n");
}
}
int main(int argc, char const *argv[])
{
//使用signal函数对产生的信号做出处理
//使用signal函数,如果信号不产生,则永远也不会执行对应的处理方式
//但是一旦信号产生了,就会立即执行对应的处理方式
//当信号产生后,会立即执行signal函数设置的处理方式,当处理方式执行完毕,又会回到之前的代码位置继续执行
//如果信号使用signal设置了处理方式,则当前程序中都有效
//一般signal函数要放在程序的最前面,保证信号产生后是按照自己设置的处理方式进行处理的
//以默认的方式处理信号
#if 0
if(signal(SIGINT, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if(signal(SIGQUIT, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if(signal(SIGTSTP, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
#endif
//以忽略的方式来处理信号
#if 0
if(signal(SIGINT, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if(signal(SIGQUIT, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if(signal(SIGTSTP, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
#endif
//SIGKILL和SIGSTOP信号只能以默认的方式处理,不能被忽略或者自定义
// if(signal(SIGKILL, SIG_IGN) == SIG_ERR)
// {
// perror("fail to signal");
// exit(1);
// }
//以用户自定义方式处理信号
if(signal(SIGINT, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if(signal(SIGQUIT, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if(signal(SIGTSTP, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
ps:剩余三种通信方式下期在做总结☺