【Linux】fork进程

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()函数

参考链接:

        Linux中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;
}

参考链接:

Linux系统僵尸进程详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值