fork工作原理
- fork()函数,调用一次返回2次,返回pid>0为父进程,pid=0为子进程。
-
Linux 的
fork()
使用是通过写时拷贝 (copy- on-write) 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术 -
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间,只有在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。即资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享
-
fork之后父子进程共享文件。fork产生的子进程与父进程有相同的文件描述符,指向相同的文件表,引用计数增加,共享文件偏移指针
-
使用虚拟地址空间,由于用的是写时拷贝 (copy- on-write) ,下图不完全准确,但可帮助理解
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值。
wait()函数
参考链接:
pipe 管道
数据从写端流入管道,从读端流出。读走了,就没了。
pipe 函数:
int pipe(int pipefd[2]);
函数调用成功返回 r/w 两个文件描述符。无需 open,但需手动 close。规定:fd[0] → r; fd[1] → w,就像 0 对应标准输入,1 对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。
参数: fd[0]: 读端。
fd[1]: 写端。
返回值:成功: 0
失败: -1 ,设置errno
代码:父子进程使用管道通信,父写入字符串,子进程读出,并打印到屏幕。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void say_err(const char str[]){
perror(str);
exit(1);
}
int main(int argc, char* argv[])
{
int ret;
int pipefd[2]; //pipefd[0]是读端,pipefd[1]是写端。
pid_t pid;
char buffer[1024];
ret = pipe(pipefd);
if(ret == -1){
say_err("pipe open error!");
}
pid = fork();
if(pid > 0){ //父进程
close(pipefd[0]); // 父亲关闭读,只写。
write(pipefd[1], "Hello pipe!\n", strlen("Hello World!\n"));
close(pipefd[1]); //写完,关闭写端。
}else if(pid == 0){ //子进程
close(pipefd[1]); // 子关闭写,只读。
int count = 0;
count = read(pipefd[0], buffer, 1024);
write(STDOUT_FILENO, buffer, count); //将从管道读到的东西,立马写到屏幕上。
close(pipefd[0]); //读完,关闭读端。
}else{ //进程创建失败。
say_err("fork create error!");
}
return 0;
}
// 能保证父进程写在前,子进程读在后吗?
// 不用保证,读端会阻塞的
// 如果子进程先执行,并且在父进程写入数据之前尝试读取管道,那么子进程将会阻塞,直到父进程写入数据或者关闭写端。
数据从写端流入管道,从读端流出。读走了,就没了。
fifo 管道:
可以用于无血缘关系的进程间通信。
创建方式:
1. 命令:mkfifo 管道名
2. 库函数:int mkfifo(const char *pathname, mode_t mode); 成功:0; 失败:-1
一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件 I/O 函数都可用于 fifo。如:close、read。write、unlink 等。
makefifo.c 代码:
int main(int argc, char* argv[])
{
int ret = mkfifo("mytestfifo", 0664);
if(ret == -1){
say_err("fifo create error!|n");
}
return 0;
}
fifo_w.c代码:
int main(int argc, char *argv[])
{
int fd, i;
char buf[4096];
if (argc < 2) {
printf("Enter like this: ./a.out fifoname\n");
return -1;
}
fd = open(argv[1], O_WRONLY); //打开管道文件
if (fd < 0)
sys_err("open");
i = 0;
while (1) {
sprintf(buf, "hello itcast %d\n", i++);
write(fd, buf, strlen(buf)); // 向管道写数据
sleep(1);
}
close(fd);
return 0;
}
fifo_r.c代码:
int main(int argc, char *argv[])
{
int fd, len;
char buf[4096];
if (argc < 2) {
printf("./a.out fifoname\n");
return -1;
}
//int fd = mkfifo("testfifo", 644);
//open(fd, ...);
fd = open(argv[1], O_RDONLY); // 打开管道文件
if (fd < 0)
sys_err("open");
while (1) {
len = read(fd, buf, sizeof(buf)); // 从管道的读端获取数据
write(STDOUT_FILENO, buf, len);
sleep(3); //多個读端时应增加睡眠秒数,放大效果.
}
close(fd);
return 0;
}
使用:
./fifo_w mytestfifo //一边写
./fifo_r mytestfifo //一边读
数据从写端流入管道,从读端流出。读走了,就没了。
mmap()
父子共享:打开的文件描述符; mmap建立的映射区(MAP_SHARED )。
#include <sys/mman.h>
void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset);
Arguments Describes (参数描述)
- 参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。
- len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
- prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。
- flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。
- 参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。
- offset参数一般设为0,表示从文件头开始映射, 代表偏移量。
返回:成功:返回创建的映射区首地址;失败:MAP_FAILED 宏
fork_mmap.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open error");
exit(1);
}
ftruncate(fd, 4); // 给temp开辟四字节的空间。
// p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork(); //创建子进程
if(pid == 0){
*p = 7000; // 写共享内存
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
var = 200;
printf("parent, *p = %d, var = %d\n", *p, var); // 读共享内存
wait(NULL);
int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}
MAP_SHARED,对共享内存所做的修改,最终会反映到物理磁盘上。进程间通讯,必须用MAP_SHARED。
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
yugong@yugong-virtual-machine:~/zsy/IPC_test/mmap$ make fork_mmap
gcc fork_mmap.c -o fork_mmap
yugong@yugong-virtual-machine:~/zsy/IPC_test/mmap$ ./fork_mmap
child, *p = 7000, var = 1000
parent, *p = 7000, var = 200
MAP_PRIVATE,对共享内存所做的修改,不会反映到物理磁盘上。MAP_PRIVATE不能用于进程间通讯。
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
yugong@yugong-virtual-machine:~/zsy/IPC_test/mmap$ make fork_mmap
gcc fork_mmap.c -o fork_mmap
yugong@yugong-virtual-machine:~/zsy/IPC_test/mmap$ ./fork_mmap
child, *p = 7000, var = 1000
parent, *p = 0, var = 200
孤儿进程
前面提到过,在 Linux 环境中,我们是通过 fork
函数来创建子进程的。创建完毕之后,父子进程独立运行,父进程无法预知子进程什么时候结束。
通常情况下,子进程退出后,父进程会使用 wait
或 waitpid
函数进行回收子进程的资源,并获得子进程的终止状态。
但是,如果父进程先于子进程结束,则子进程成为孤儿进程。孤儿进程将被 init 进程(进程号为1)领养,并由 init 进程对孤儿进程完成状态收集工作。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。
而如果子进程先于父进程退出,同时父进程太忙了,无瑕回收子进程的资源,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程,如下图所示:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
} else if(pid == 0) {
sleep(1);
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
}
// for循环
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
}
return 0;
}
参考链接: