聊一聊Linux的进程创建

进程作为操作系统中的基本管理单位,起着重要作用,那么进程到底是怎么被创建的,创建进程时到底都发生了些什么,本文将会讨论这些问题。

进程是如何被创建的

从现象上看,当我们执行一个main方法时,会创建一个进程,当我们执行一条linux命令时,也会创建一个进程。要说进程究竟是如何被创建的,这里给出两个回答:

  1. 进程是被它的父进程创建的
  2. 进程是被fork出来的

这两个回答都对,只是从不同层面回答了这个问题。

进程是被它的父进程创建的

如果去一台linux机器上ps -eaf一下,你会看到每个进程都会对应一个父进程,下图中第二列为当前进程的pid,第三列为父进程的pid。并且所有的进程都会有一个祖先进程,即pid为1的进程。

如果将这些进程的父子关系组织起来,那它们应该是一个树状的结构,可以看到,所有进程都有一个公共的根进程,即pid为1的systemd:

那么问题来了,这个pid为1的进程到底是何方神圣?

pid为1的进程是什么?

Pid为1的进程被称为init进程,它是系统启动时最早启动的用户级进程,是所有其他用户级进程的祖先。init进程负责执行系统引导过程中所需的初始化任务,然后它会启动其他系统服务和用户级别的进程。实际上,init进程有很多不同的实现,如SysVinit, launched, upstart,systemd等,现行的linux操作系统重,基本systemd已成为主流。

进程是被fork出来的

如前文所述,进程是被它的父进程创建的,而创建的具体方式,正是fork. 理论上来讲,fork的时候子进程会得到父进程地址空间的一个副本,包括代码段、数据段、堆栈等,但是由于通常子进程和父进程执行并不是同样的内容,而是会通过exec系统调用装填一段新的代码进去,因此出于效率考虑,linux使用了一种叫做写时复制(Copy-On-Write)的概念,即当fork发生时,子进程并不会完全拷贝一份父进程地址空间的内容,而是把父进程的地址空间内容改为只读状态,此时如果父进程或者子进程尝试修改这些区域,内核会为要被修改的区域所在的页制作一个副本。

COW机制

我们在linux上使用c语言创建一个进程,并fork出一个子进程,来解释这个问题。

c 复制代码
 #include <stdio.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <stdint.h>
 ​
 ​
 int main() {
         int value = 42;
 ​
         pid_t pid = fork();
 ​
         if (pid == 0) {
                 // 子进程
                 printf("Child process. PID: %d\n", getpid());
                 printf("Child: Initial value = %d\n", value);
 ​
                 // 修改值,触发COW机制
                 value = 99;
                 printf("Child: Modified value = %d\n", value);
 ​
         } else if (pid > 0) {
                 // 父进程
                 sleep(5);
                 printf("Parent process. PID: %d\n", getpid());
                 printf("Parent: Initial value = %d\n", value);
 ​
                 // 等待子进程执行完毕
                 wait(NULL);
 ​
                 // 输出修改后的值,观察是否受到子进程修改的影响
                 printf("Parent: After child's modification, value = %d\n", value);
         } else {
                 // 错误处理
                 fprintf(stderr, "Fork failed\n");
                 return 1;
         }
 ​
         return 0;
 }

在上面代码中,我们在父进程中初始化了一个int类型的value变量,值为42,并在子进程中修改value, 最后在父进程中观察value的值。理论上来讲,由于COW机制,当子进程修改value时,操作系统会将父进程中value所在的页复制出来,并分配给子进程,因此,子进程对value的修改并不会影响到父进程中的value.

输出如下:

ini 复制代码
 Child process. PID: 2059
 Child: Initial value = 42
 Child: Modified value = 99
 Parent process. PID: 2058
 Parent: Initial value = 42
 Parent: After child's modification, value = 42

可以看到,父进程中的value依然是42.

需要注意的是,COW对于共享型的内存映射(shared mmap)是不生效的(毕竟听名字就知道,它是进程共享的),举个🌰:

arduino 复制代码
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <sys/mman.h>
 #include <sys/wait.h>
 ​
 int main() {
     // 创建匿名映射,大小为一个整数的大小
     size_t size = sizeof(int);
     int *shared_data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
 ​
     if (shared_data == MAP_FAILED) {
         perror("mmap");
         exit(EXIT_FAILURE);
     }
 ​
     // 初始化共享数据
     *shared_data = 42;
 ​
     // 使用 fork() 创建子进程
     pid_t pid = fork();
 ​
     if (pid == 0) {
         // 这是子进程
         printf("Child process. PID: %d\n", getpid());
 ​
         // 修改共享数据
         *shared_data = 99;
 ​
         // 输出子进程的共享数据
         printf("Child: Modified value = %d\n", *shared_data);
 ​
         // 释放映射
         munmap(shared_data, size);
 ​
     } else if (pid > 0) {
         // 这是父进程
         printf("Parent process. PID: %d\n", getpid());
 ​
         // 输出父进程的共享数据
         printf("Parent: Initial value = %d\n", *shared_data);
 ​
         // 等待子进程执行完毕
         wait(NULL);
 ​
         // 输出修改后的值
         printf("Parent: After child's modification, value = %d\n", *shared_data);
 ​
         // 释放映射
         munmap(shared_data, size);
 ​
     } else {
         // 错误处理
         perror("fork");
         exit(EXIT_FAILURE);
     }
 ​
     return 0;
 }
 ​

对于上面代码,我们创建了一个匿名的共享的mmap,它的初始值为42,并在子进程中修改该内存中的值为99,那么该值的变化最后将会反映到父进程中,输出如下:å

yaml 复制代码
 Parent process. PID: 2364
 Child process. PID: 2365
 Child: Modified value = 99
 Parent: Initial value = 99
 Parent: After child's modification, value = 99

如果我们把第十行的SHARED改为PRIVATE:

arduino 复制代码
     int *shared_data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

那么子进程对共享内存的修改将不会影响到父进程中的该值(当然了,多进程之间使用匿名mmap的用之一就是用于通信,如果设为PRIVATE就失去了其意义,这里这么做只是为了说明COW的效果。)

参考

coolshell.cn/articles/17...

相关推荐
且从容.4 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道4 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.7 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落9 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨9 小时前
WSL2的安装与使用
linux·wsl
微小的鱼9 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng10 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮10 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky1 天前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默1 天前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu