Linux 进程的创建

 目录

进程创建

fork()函数

fork返回值

fork写时拷贝

fork失败原因

fork用法


进程创建

Linux 中我们可以说一个进程就是一个PCB, 即 一个task_struct, 那么创建进程也就是创建PCB, 即是创建task_struct

Linux 中说到进程创建,  就不得不提到 fork()函数. fork()在Lnux下是非常重要的一个函数 .

fork()函数

从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

fork()在函数内部会调用clone这个系统调用接口

pid_t  fork ()

头文件: unistd.h

fork返回值

fork函数返回值 : (返回值类型为pid_t, 实际等同于int)

  • 子进程在执行fork()时返回 0
  • 父进程在执行fork()时, fork()创建子进程, 返回子进程的PID (PID是一个大于0的整数)
  • 父进程在用fork()创建子进程失败时返回 -1

因为fork运行有多种结果, 所以往往fork之后要根据fork的返回值进行分流(例如用 if 写多个分支), 来看个例子 .

testfork.c 如下

#include<stdio.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
    }
    else if(pid == 0){
        printf("子进程\n");
    }
    else{
        printf("父进程\n");
    }
    return 0;
}

编译执行如下, 可以看到, 当父进程用fork() 创建子进程成功后, 返回了其子进程的pid, 然后继续执行, 直到执行打印语句后子进程才执行, 如下 :

但并不是父进程创建了子进程, 父进程就一定会先执行完,才执行子进程, 也可能是父进程执行到一半, 甚至刚调用fork()创建完子进程后, 就立即转而执行子进程. 这取决于CPU的调度. 比如说下面这段代码 .

#include<stdio.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
    }
    else if(pid == 0){
        printf("子进程执行\n子进程pid:%d\n", getpid());
    }
    else{
        printf("父进程开始执行\n");
        sleep(5);
        printf("父进程执行\n父进程pid:%d\n", getpid());
        printf("父进程运行结束\n");
    }
    return 0;
}

可以看到, 父进程执行到一半开始执行子进程了, 就此次运行结果分析, 由于父进程中sleep()函数, 致使父进程进入睡眠状态

(sleeping)(这种睡眠是可中断的, 当sleep()执行完, 就会中断睡眠, 进入就绪状态(或者说进入运行队列), 等待分配时间片), 子进程

当被创建后, 一直处于就绪状态(一直处于运行队列中), 等待分配时间片, 当父进程睡眠时, 子进程拿到了时间片, 子进程执行 . 当子

进程执行完, 父进程拿到时间片后, 父进程继续运行 . 

所以就有, fork创建子进程之前, 父进程独立运行, 创建子进程之后, 谁先运行取决于调度器的调度

进程调用fork,  内核会做出以下操作

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程 ( 此时已经创建了子进程的PCB即Linux下的task_struct )
  • 添加子进程到系统进程列表当中 ( 即添加子进程PCB)
  • fork返回,调度器开始调度

fork写时拷贝

fork 创建子进程采取分时拷贝的策略 .

即,  父进程创建子进程时,只创建了子进程的task_struct(PCB), 并没有直接给子进程开辟内存来拷贝数据,而是跟父进程

一样映射到同一位置,但是如果父进程或子进程有一方想要修改内存中的数据时,那么对于改变的这块内存,需要重新给

子进程开辟内存,并且更新子进程页表信息. 这样做, 提高创建子进程的性能, 并且能节省内存 . 如下图 :

                      图1. 父子进程共享代码与数据                                                     图2. 父子进程代码共享, 数据独立

图里涉及到了虚拟内存与分页式内存管理的内容, 简单说一下, 分页式内存管理可以将一段程序加载到不连续的物理空间上,但是

从虚拟地址空间来看依旧是连续的, 用以解决内存使用率低的问题 .

mm_struct结构体也叫内存描述符,其中记录虚拟内存各个段的起始地址, 结束地址, 通过这种方式描述了进程的虚拟地址空间, 

每一个进程都会有唯一的mm_struct结构体, mm_struct记录在task_struct中.

页表: 页表中存储的是虚拟地址和物理地址的映射关系, 即页号到物理块的地址映射. 通过虚拟地址得到页号与页内地址(或者叫页内偏移),  在页表中通过页号找到物理块号.  然后,  物理地址 = 物理块号 x 页面大小 + 页内偏移  就得到了物理地址

来段代码感受一下

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    int data = 0;
    if(pid == -1){
        perror("fork erro");
    }
    else if(pid == 0){
        printf("子进程执行\n");
        data = 10;
        printf("data = %d\n", data);
        printf("data地址: %p\n", &data);
    }
    else{
        sleep(2);
        printf("父进程执行\n");
        printf("data = %d\n", data);
        printf("data地址: %p\n", &data);
    }
    return 0;
}

代码中, 先让父进程睡上2秒, 这时会执行子进程, 子进程修改了data的值为10, 但子进程结束后, 父进程继续执行打印出的data

还是0, 两个进程所打印的值不同,  但父子进程中data的地址都是一样的.  我们知道数据不同, 则数据一定存储在不同的物理地址

上, 打印的的变量地址依旧相同, 这是因为取地址&得到的并不是物理地址(在所有有关地址的操作中, 我们只能接触到虚拟地址),

而是虚拟地址, 虚拟地址虽然相同, 但是父子进程有着不同的mm_struct, 即有着不同的页表, 这父子进程的data相同的(虚拟)地址

通过不同的页表映射到不同的物理地址上.

运行结果如下图:

fork失败原因

  • 系统进程数达到太多, 达到上限(系统会有一个进程数的限定, 可以修改)
  • 内存不足 (fork创建子进程需要创建新的PCB, 写时拷贝可能还会分配新的数据空间)

fork用法

fork()函数当然不是为了创建子进程而创建子进程, 创建子进程目的有两种:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段.  

    例如: 父进程等待客户端请求,生成子进程来处理请求。
     
  • 一个进程要执行一个不同的程序 . 例如: 子进程从fork返回后,调用exec函数(用来做进程替换的函数, 下面说)替换子进程. 比

    如, 我们用的Shell就是一个程序,   一般为默认为bash, 我们执行一些非Shell内建命令时, 实际就是一个Shell创建子进程, 再进

    行进程替换的过程
发布了223 篇原创文章 · 获赞 639 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/103302804