进程间通信方式
IPC方法:linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
内核空间中的一块缓冲区大小为4096
在进程间完成数据传递需要借助操作系统提供的特殊的方法,文件、管道、信号、共享内存、消息队列、套接字、命名管道等。
1、管道(使用简单)
2、信号(开销最小)
3、共享映射区(无血缘关系)
4、本地套接字(最稳定)
管道
调用pipe系统函数即可创建一个管道。本质是一个伪文件(内核缓冲区),有两个文件描述符引用,一个表示读端,一个表示写端。规定数据从管道的写端流入管道,从读端流出。
管道的原理:为内核使用环形队列机制,借助内核缓冲区(4k)实现。
局限性:数据不能进程自己写,自己读;管道中数据不可反复读取,一旦读走,管道中不再存在;采用半双工通信方式,数据只能在单方向上流动;只能在有公共祖先的进程间使用管道。
makefifo f1:创建的管道可以作用在不具有血缘关系的两个进程之间。
7种文件类型,除文件、软连接、硬链接外其余为伪文件
int pipe(int fd[2]); 0表示读端,成功返回0,失败返回-1
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int ret;
int fd[2];
pid_t pid;
char buf[1024];
ret = pipe(fd);
if(ret == -1){
sys_err("pipe error");
}
pid = fork();
if(pid > 0){
close(fd[0]);//父进程关闭读端
write(fd[1], "hello pipe", strlen("hello pipe"));
close(fd[1]);
}else if(pid == 0){
close(fd[1]);//子进程关闭写端
ret = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
close(fd[0]);
}
return 0;
}
父进程写,子进程读
运行结果为:
ymyy@ymyy-virtual-machine:~/systemcode$ ./pipe
hello pipe
管道的读写行为
读管道:
1、管道中有数据,read返回实际读到的字节数。
2、管道中无数据,管道写端被全部关闭,read返回0(读到文件结尾),写端没有被全部关闭,read阻塞等待(不久的将来可能数据抵达)
写管道:
1、管道读端全部被关闭,进程异常终止(也可捕捉SIGPIE信号,使进程不终止)
2、管道读端没有全部关闭,管道已满,write阻塞,管道未满,write 将数据写入,并返回实际写入的字节数。
小练习:使用管道实现父子进程间通信,完成 ls | wc -l。假定父进程实现 ls,子进程实现 wc。
ls 命令正常会将结果写在 stdout,但现在会写入管道的写端:wc-l 正常应该从 stdin 读取数据,但此时会从管道的读端读。
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int ret;
pid_t pid;
int fd[2];
char buf[1024];
ret = pipe(fd);
if(ret == -1){
sys_err("pipe error");
}
pid = fork();
if(pid > 0){
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
}else if(pid == 0){
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}else{
sys_err("fork error");
}
return 0;
}
执行结果为:(为避免bash进程先于子进程执行完抢占终端,可以交换父子进程的内容,即子进程写,父进程读,bash进程在父进程结束后抢占终端)
ymyy@ymyy-virtual-machine:~/systemcode$ ./ls-wc-l
ymyy@ymyy-virtual-machine:~/systemcode$ 37
ls
block_readtty fcntl.c fork_exec lseek_test1 ls-R.c mycp.c pipe.c zoom_test
block_readtty.c fcntl_dup fork_exec.c lseek_test1.c ls-wc-l nonblock_readtty unlink_exe zoom_test.c
dup fcntl_dup.c loop_fork lseek_test.c ls-wc-l.c nonblock_readtty.c unlink_exe.c
dup.c fork loop_fork.c lseek.txt makefile out waitpid_while
fcntl fork.c lseek_test ls-R mycp pipe waitpid_while.c
兄弟进程间通信
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/wait.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int fd[2];
int ret, i;
pid_t pid, wpid;
ret = pipe(fd);
if(ret == -1){
sys_err("pipe error");
}
for(i = 0; i < 2; i++){//表达式2出口,仅限父进程使用
pid = fork();
if(pid == -1){
sys_err("fork error");
}
if(pid == 0){//子进程出口
break;
}
}
if(i == 2){
close(fd[0]);//一定要关闭父进程管道,否则数据流向父进程,不能保证单向流动
close(fd[1]);
wait(NULL);
wait(NULL);
/*回收子进程的另一种写法
while((wpid = waitpid(-1, NULL, WNOHANG)) != -1){
if(wpid > 0){
printf("wait child %d\n", wpid);
}else if(wpid == 0){
continue;
}
}
*/
} else if(i == 0){//兄进程ls
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
}else if(i == 1){//弟进程wc-l
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}
return 0;
}
结果为:
ymyy@ymyy-virtual-machine:~/systemcode$ ./lswcl-brother
42
ymyy@ymyy-virtual-machine:~/systemcode$ ls | wc -l
42
多个读写端操作管道和管道缓冲区大小
一个父进程读,两个子进程写,一个读端两个写端。调控写入顺序。
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/wait.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int fd[2];
int ret, i;
pid_t pid, wpid;
int n;
char buf[1024];
ret = pipe(fd);
if(ret == -1){
sys_err("pipe error");
}
for(i = 0; i < 2; i++){//表达式2出口,仅限父进程使用
pid = fork();
if(pid == -1){
sys_err("fork error");
}
if(pid == 0){//子进程出口
break;
}
}
if(i == 0){
close(fd[0]);
write(fd[1], "1.hello\n", strlen("1.hello\n"));
}else if(i == 1){
close(fd[0]);
write(fd[1], "2.hello\n", strlen("2.hello\n"));
}else{
close(fd[1]);
sleep(1);//父进程需要等待两个子进程执行完毕后再读
n = read(fd[0], buf, 1024);
write(STDOUT_FILENO,buf, n);
wait(NULL);
wait(NULL);
}
return 0;
}
结果为:
ymyy@ymyy-virtual-machine:~/systemcode$ ./pipe3
1.hello
2.hello
命名管道fifo的创建和原理
管道只能用于有血缘关系的进程间,但通过FIFO,不相关的进程也能交换数据。FIFO是linux文件类型中的一种,但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中的一条通道。各个进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
创建方式:mkfifo 管道名 成功:0,失败:-1。 一旦使用 makefifo 创建了一个FIFO,就可以用open打开它,常见的文件I/O函数都可用于fifo。