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

相关推荐
草莓熊Lotso43 分钟前
Linux 基础 IO 初步解析:从 C 库函数到系统调用,理解文件操作本质
linux·运维·服务器·c语言·数据库·c++·人工智能
Cx330❀1 小时前
从零实现Shell命令行解释器:原理与实战(附源码)
大数据·linux·数据库·人工智能·科技·elasticsearch·搜索引擎
学嵌入式的小杨同学7 小时前
从零打造 Linux 终端 MP3 播放器!用 C 语言实现音乐自由
linux·c语言·开发语言·前端·vscode·ci/cd·vim
EverydayJoy^v^7 小时前
RH134学习进程——十二.运行容器(1)
linux·运维·容器
syseptember7 小时前
Linux网络基础
linux·网络·arm开发
zl_dfq8 小时前
Linux 之 【多线程】(线程的概念、Linux中的线程、页表)
linux
郝亚军9 小时前
如何在Ubuntu和win10/11之间通过samba访问对方的文件
linux·服务器·ubuntu
曦云沐9 小时前
【避坑指南】Ubuntu更新报错“Repository is not signed”的快速修复
linux·ubuntu·docker
带土110 小时前
10. .out文件
linux
STCNXPARM10 小时前
Linux camera之V4L2子系统详解
android·linux·camera·v4l2架构