聊一聊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...

相关推荐
一一Null38 分钟前
Linux-网络管理
linux·运维·服务器
☞下凡☜2 小时前
C语言(20250711)
linux·c语言·开发语言
GUET_一路向前3 小时前
【git】在Linux系统下clone指定分支
linux·运维·git
云心雨禅4 小时前
Ubuntu GRUB菜单密码重置教程
linux·运维·ubuntu
温暖的苹果4 小时前
【linux V0.11】init/main.c
linux·c语言
iCxhust5 小时前
一个用于在 Ubuntu 22.04.3 LTS 上显示文件系统超级块信息的 C 程序
linux·c语言·ubuntu
Bella的成长园地5 小时前
linux 系统依赖包查询命令汇总
linux·运维·服务器
Arthurmoo6 小时前
Linux系统集群部署模块之Keepalived双机热备
linux·git·github
hweiyu006 小时前
Linux 命令:uname
linux·运维·服务器
大时代11056 小时前
Linux 基础 IO
linux