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

相关推荐
被遗忘的旋律.1 天前
Linux驱动开发笔记(十)——中断
linux·驱动开发·笔记
凡间客1 天前
Linux防火墙-Firewalld
linux·运维·服务器
nnerddboy1 天前
Linux嵌入式自学笔记(基于野火EBF6ULL):1.配置环境
linux·笔记·单片机·嵌入式硬件
Justin_191 天前
Linux防火墙firewalld
大数据·linux·运维
皆过客,揽星河1 天前
Linux上安装MySQL8详细教程
android·linux·hadoop·mysql·linux安装mysql·数据库安装·详细教程
青草地溪水旁1 天前
Unix/Linux 系统中的 `writev` 系统调用
linux·unix·writev
Justin_191 天前
Linux-Shell编程之sed和awk
linux·运维·服务器
Akshsjsjenjd1 天前
深入理解 Shell 循环与函数:语法、示例及综合应用
linux·运维·自动化·shell
塔中妖1 天前
【华为OD】Linux发行版的数量
linux·算法·华为od
半桔1 天前
【Linux手册】消息队列从原理到模式:底层逻辑、接口实战与责任链模式的设计艺术
java·linux·运维·服务器