进程控制----进程创建

发布于:2025-07-08 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、fork 函数基础概念

作用:创建一个新进程(子进程),与父进程共享代码段,数据段采用写时拷贝(Copy-on-Write)机制。
特点

  • 调用一次,返回两次(父进程和子进程各返回一次)。
  • 父子进程拥有独立的进程控制块(PCB)和虚拟地址空间,但初始时共享物理内存中的代码和数据(通过页表映射)。

二、fork 调用时操作系统的核心操作

1.分配内核资源

  • 为子进程创建独立的 PCB(进程控制块)和虚拟地址空间页表,构建虚拟地址与物理地址的映射关系。
  • 子进程的 PCB 中包含进程状态、优先级、文件描述符表等信息。

2.拷贝父进程数据结构

  • 复制父进程的部分数据结构(如打开的文件描述符、进程环境变量等)到子进程的 PCB 中。

3.将子进程加入系统进程列表

  • 将子进程的 PCB 添加到操作系统的进程调度队列中,等待 CPU 调度。

4.fork 返回与进程调度

  • 父进程中 fork 返回子进程的 PID(非零值),子进程中 fork 返回 0。
  • 调度器开始调度父子进程,两者可能以任意顺序执行。

三、代码示例:fork 的执行效果

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("pid: %d before!\n", getpid()); // 父进程执行一次
    
    fork(); // 创建子进程,此后父子进程并行执行
    
    printf("pid: %d after!\n", getpid()); // 父子进程各执行一次
    return 0;
}

输出结果

pid: X before!  // X为父进程PID,仅打印一次
pid: X after!   // 父进程打印
pid: Y after!   // Y为子进程PID,子进程打印

解析

  • fork()调用前,只有父进程执行,打印before
  • fork()调用后,父子进程同时执行后续代码,因此after会打印两次。

四、fork 函数的返回值

进程类型 返回值 含义
父进程 子进程的 PID(>0) 用于标识新创建的子进程
子进程 0 标识自身为子进程
失败 -1 如系统进程数超限、内存不足等

使用场景
通过判断返回值实现父子进程逻辑分流:

pid_t pid = fork();
if (pid < 0) {
    perror("fork failed"); // 处理错误
} else if (pid == 0) {
    // 子进程逻辑(如执行不同任务)
} else {
    // 父进程逻辑(如等待子进程结束)
}

 五、写时拷贝(Copy-on-Write, COW)技术

核心思想:父子进程初始时共享物理内存中的代码和数据,仅在需要修改时才复制副本,避免无意义的内存拷贝。

共享机制实现

  • 父子进程的页表初始时指向相同的物理内存页,且页表权限设为只读(代码段默认只读,数据段临时设为只读)。
  • 当任意进程尝试写入数据段时,触发缺页中断(页权限冲突)。

写时拷贝触发流程
 

  • 操作系统检测到写操作,为写入方分配新的物理内存页。
  • 将原有数据拷贝到新页,更新页表映射关系为可写,允许进程修改新页的数据。
  • 代码段因权限固定为只读,若强行写入会触发段错误(Segmentation Fault),而非写时拷贝。

优势

  • 减少内存占用:避免复制未修改的数据,尤其适用于大内存场景。
  • 提升效率:延迟内存分配,仅在必要时执行拷贝。

六、fork 失败原因 

  • 系统进程数超限:内核维护的进程表条目或 PID 资源耗尽。
  • 内存不足:无法为子进程分配必要的内核结构或用户空间内存。
  • 通过判断fork返回值为 - 1 并调用perror可获取具体错误信息。

七、多进程创建

代码示例:批量创建子进程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

void runChild(){
  int cnt=10;
  while(cnt){
    printf("I'm child,pid:%d,ppid:%d\n",getpid(),getppid());
    sleep(1);
    --cnt;
  }
  exit(0);
}

int main(){
  int n=5;
  for(int i=0;i<n;i++){
    pid_t id=fork();
    if(id==0){
      //child
      runChild();
    }
  }
  sleep(1000);
}

代码执行逻辑解析

1.父进程循环创建子进程

  • 每次循环调用 fork() 时,父进程复制出一个子进程。
  • 父进程行为fork() 返回子进程的 PID(正数),继续执行循环,创建下一个子进程。
  • 子进程行为fork() 返回 0,进入 if (id == 0) 分支,执行 runChild() 函数,执行完毕后通过 exit(0) 退出。

2.子进程独立运行

  • 每个子进程执行 runChild() 时:
    • 每秒输出自身 PID(getpid())和父进程 PID(getppid())。
    • 循环 10 次后自动退出,避免成为僵尸进程。

3.父进程休眠保持存活

  • 循环结束后,父进程通过 sleep(1000) 长时间休眠,确保子进程有足够时间运行。
  • 若父进程提前退出,子进程会成为孤儿进程,被 init 进程(PID=1)接管,但此处通过休眠避免了这一情

运行结果观察与分析

通过命令实时查看进程信息

  • 另开终端窗口,使用以下命令每秒刷新进程列表:
    while :; do ps ajx | head -1 && ps ajx | grep createMulProcess | grep -v grep; sleep 1; done  
    
  •  输出示例

  • PID 分配的动态特性

    • 多次运行代码时,子进程 PID 后两位可能不连续(如第一次为 28、29、30、32、31,第二次为 28、29、30、31、32)。
    • 原因:PID 由系统动态分配(基于 PID 池循环使用),与创建顺序无固定关联,体现了操作系统的资源管理机制。

多进程执行顺序的不确定性

调度器的作用

  • 父子进程或兄弟进程的执行顺序由操作系统的 进程调度器 决定,无法预先确定。
  • 新创建的进程会被放入 运行队列 等待调度,执行顺序取决于:
    • 进程优先级(默认相同)。
    • 调度算法(如轮转调度、优先级调度)。
    • 系统负载和资源竞争情况。

示例场景

  • 若子进程 1 先被调度,可能先输出;若子进程 3 先被调度,可能先输出,完全随机。
  • 这一特性是多进程 / 多线程编程的核心难点之一,需通过同步机制(如信号量、管道)协调顺序。

 


网站公告

今日签到

点亮在社区的每一天
去签到