当前位置:首页>Linux>OS18.【Linux】进程基础知识(2) PID、PPID、fork、vfork、父子进程

OS18.【Linux】进程基础知识(2) PID、PPID、fork、vfork、父子进程

  • 2026-10-11 06:44:21
OS18.【Linux】进程基础知识(2) PID、PPID、fork、vfork、父子进程

目录

1.ps ajx命令是如何获取PID的

2.实验:编写代码,通过系统调用来获取PID

pid_t类型的本质

strace命令

3.PPID

父进程和子进程

PPID

结论

4.使用fork()创建子进程

测试代码1

测试代码2

5.回答与fork()有关的5个问题

6.简单理解fork()是怎么创建子进程的?

父子进程的代码段分析 

父子进程的数据段分析

结论

7.使用vfork()创建子进程

vfork()与fork()的区别

8.总结:创建进程的两种方法

9.练习题

10.进程树、所有进程的祖先

使用pstree查看进程树

systemd进程是由谁创建的呢?


承接OS17.【Linux】进程基础知识(1)文章,继续讲解进程的基础知识

1.ps ajx命令是如何获取PID的

一般情况下,进程的PCB是通过双向链表连接起来的,PCB中含有指向该进程的代码段和数据段

的指针,如下图所示:

进程由操作系统管控,那么ps ajx只能通过系统调用接口来遍历这个双向链表,由于PCB中存储着进程的PID,那么ps ajx就能获取进程的各个信息,之后打印到显示器上

2.实验:编写代码,通过系统调用来获取PID

手册中对getpid()函数的描述:

(注:查系统调用一定要查2号手册) 

作用:返回调用进程的PID

返回类型:pid_t

需要包含两个头文件: <sys/types.h>和<unistd.h>

pid_t类型的本质

有几种方法

1.GNU官网www.gnu.org Process-Identification对pid_t的解释

Data Type: pid_t

The pid_t data type is a signed integer type(有符号整型) which is capable of representing a process ID. In the GNU C Library, this is an int.

2.也可以使用C++的typeid来打印pid_t的类型

#include<iostream>#include<typeinfo>#include<sys/types.h>using namespace std; intmain(){pid_t id;    cout << "Type of pid_t: " << typeid(id).name() << endl;    return 0;}

运行结果:

3.使用man命令

man 7 pid_t

 (7号手册内容是系统数据类型)

也提到了是int类型

4.其实pid_t也是被重定义的

阅读linux内核源代码,/usr/include/sys/type.h下有说明:

#ifndef __pid_t_definedtypedef __pid_t pid_t;# define __pid_t_defined#endif

结论: 可以用%d打印pid_t类型

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){        printf("The process's PID is %d",getpid());        getchar();//防止进程退出        return 0;}

运行结果:

strace命令

作用:跟踪系统调用和信号

例如查看上方代码生成的可执行程序的系统调用:

往下翻可以看到getpid()

3.PPID

父进程和子进程

从名称上来看,父进程创建子进程,所以父子进程是相对的

PPID

PPID的全称是Parent Process ID,即父进程的PID

可以使用getppid()函数来获取PPID:

作用:返回调用进程的父进程的PID

返回类型:pid_t

需要包含两个头文件: <sys/types.h>和<unistd.h>

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){        printf("The process's PID is %d,PPID is %d\n",getpid(),getppid());        return 0;}

运行结果:

第一次运行:

第二次运行:

第三次运行:

发现:PID一直在变,但PPID没有变

查看PID为23811是哪个进程:

为bash命令行解释器进程,是合情合理的,因为bash作为父进程,通过./a.out来创建子进程

如果重新连一下服务器,再查bash进程的PID:

ps ajx | head -1 && ps ajx | grep "bash"

会发现PID变了

结论

1.bash作为父进程,通过./a.out来创建子进程 

2.连接远程服务器会创建bash进程

4.使用fork()创建子进程

查下手册中对fork()的描述:

作用:

fork() creates a new process by duplicating(复制) the calling process.  The new process is referred to as the child process.  The calling process is referred to as the parent process. 

通过复制调用的进程来创建新进程,新进程指的是子进程,调用的进程指的是父进程

返回类型:pid_t

需要包含两个头文件: <sys/types.h>和<unistd.h>

返回值:On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  On failure, -1 is returned in the parent, no child process is created, and errno is set  appropriately.

会发现fork()如果成功创建子进程,会返回两个返回值,父进程得到的返回值是子进程的PID,子进程的得到的返回值是0,这和以前C语言中将的函数的返回值只有一个是不一样的

fork()如果无法创建子进程,父进程得到的返回值是-1,errno 会被设置为相应的错误码

测试代码1

既然说fork()如果成功创建子进程,会返回两个返回值,那就根据这个特点写一个父子进程分流的代码:

#include <stdio.h>#include <sys/types.h>#include <unistd.h>int main(){printf("该进程的PID为%d,PPID为%d\n",getpid(),getppid());	pid_t ret_id=fork();if (ret_id==0)	{while(1)		{printf("子进程正在运行,PID为%d,PPID为%d\n",getpid(),getppid());	sleep(3); 		}		} else if (ret_id>0)	{while(1)		{printf("父进程正在运行,PID为%d,PPID为%d\n",getpid(),getppid());	sleep(3); 		}	 	}else	{printf("error!");	}return 0;}

运行结果:

会发现两个死循环都在运行,显然分为两个执行流

分析两个执行流:

1.父进程的PID是子进程的PPID

2.执行流程

父子进程会分流 

3.父子进程的执行顺序

父子进程执行顺序是随机的,由进程调度所决定的

测试代码2

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){    printf("teststring1\n");    fork();    printf("teststring2\n");    sleep(3);}

运行结果:

现象:

1.teststring1打印了一次,但teststring2打印了两次

2.打印teststring1一次,teststring2两次后停顿3s后退出

分析:

根据测试代码1提到的分流原则,有:父进程从printf("teststring1\n")执行,子进程从fork()后的printf("teststring2\n")开始执行,最后父子进程都sleep(3),近似同时退出(发现父子进程的代码是共享的这个后面会再次提到) 

5.回答与fork()有关的5个问题

1.为什么fork()成功创建子进程后,要给父子进程返回不同的值?

内核开发者设计创建子进程函数的目的:让子进程执行和父进程不同的代码,可以根据父子进程的得到访返回值的不同,采用if-else分流父子进程

2.fork()成功创建子进程后为什么要给父进程返回子进程的PID?

一个父进程可以有多个子进程,由于每个进程的PID是独一无二的,因此fork()给父进程返回子进程的PID是为了区分不同的子进程

反过来,一个子进程只能有一个父进程 

3.fork()成功创建子进程后为什么要给子进程返回0?

一个子进程只会有一个父进程,PID==0总是由内核交换进程使用,所以一个子进程的PID不可能为0

4.fork()是如何做到返回两次值的?

一般而言父子进程的代码是共享的,那么父子进程各执行一次fork()里面的return,因此return两次

而且代码段是不能被修改的,具体的原因和保护模式有关,这里不介绍

而且保护模式下的代码段只定义了这三个属性:accessed (访问,A), read enable (可读,R), and conforming (一致,C).

(来自Intel® 64 and IA-32 Architectures Software Developers Manual, Volume 3A: System Programming Guide, Part 1) 

而且在保护模式下操作系统对内存是分段管理的,每个段的属性存储在段描述符中,Intel手册中给出了详细内容:

 代码段不可修改也和DPL(Descriptor Privilege Level)有关,其控制对段的访问

5.对于pid_t ret_id=fork(),变量ret_id为什么会有两个不同的值?

暂且不回答,在进程地址空间会提到原因

6.简单理解fork()是怎么创建子进程的?

会议进程的组成,对于子进程,有以下公式:

子进程=子进程的PCB+子进程的代码段和数据段

父子进程的代码段分析 

因此fork()创建子进程会创建子进程的task_struct,并向task_struct写入数据

上面提到了,父子进程的代码段是共享的,因此子进程的task_struct的执行代码段的指针指向父进程的代码段

这样父子进程都有独立(即隔离)的task_struct,可以被CPU调度执行了

注:任何平台的进程之间都是独立的,具体和保护模式有关

父子进程的数据段分析

数据段可读可写,一开始(子进程刚刚创建时),父子进程的数据段是共享的,当子进程需要修改数据段中的内容时会发生写时拷贝(在文章CD40.【C++ Dev】string类的模拟实现(4)(operator=、拷贝构造函数的现代写法、写时拷贝的简单了解)提到过),操作系统会拷贝那一部分数据供子进程修改,保证父进程的数据不受影响

例如子进程的pid_t肯定要写时拷贝 

注意:没有必要将父进程数据段的所有内容拷贝一份给子进程,因为这子进程可能只会使用一部分数据,导致内存资源被浪费

结论

结论: 用fork()创建子进程时,父子进程共用同一份代码,数据以写时拷贝的方式各自私有

7.使用vfork()创建子进程

作用: 创建子进程并阻塞(block)父进程

使用前包含<unistd.h>头文件

vfork()与fork()的区别

1.vfork直接使用父进程存储空间,不用拷贝

2.vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行(之前父进程被阻塞了)

8.总结:创建进程的两种方法

方法1.通过bash执行可执行文件(指令级别),其实bash也是通过fork()来创建子进程的

方法2.使用fork()或vfork()创建子进程(代码级别)

9.练习题

不算main 这个进程自身,到底创建了多少个进程?

int main(int argc, char* argv[]){   fork();   fork() && fork() || fork();   fork();}

分析:

注意&&和||短路运算的特点,如果记不得的话可以反汇编ELF文件看看编译器是怎么处理的:

objdump -d a.out

test %eax,%eax是检查fork()的返回值是否为0,如果为0,则设置标志位ZF=1,如果不为0,则设置标志位ZF=0

test指令内部逻辑:

Intel开发手册是这样说明的:

OpcodeInstructionOp/  En64-Bit  ModeCompat/  Leg ModeDescription
85 /r
TEST r/m32, r32
MR
Valid
Valid
AND r32 with r/m32; set SF, ZF, PF according to result.

那么test %eax,%eax会将eax寄存器的值和自己进行与运算,计算的结果用于设置ZF,不修改eax寄存器的值

答案是19

10.进程树、所有进程的祖先

使用pstree查看进程树

上面说到了: 子进程是由父进程创建而来的

那么可以自然得出父进程是由父父进程创建而来的,父父进程是由父父父进程创建而来的......

按照上面这样思考,向上逆推可以得出系统中所有进程的组织方式,那么就可以理出一个进程树:

注: 进程树的图来自The Question Nobody Ever Explains: Where Does the Kernel End? - YouTube,中文翻译见无人解答的问题:内核的终点在哪里?- Bilibili

在linux上可以通过以下命令看这个进程树:

sudo pstree -p -c #使用-p显示 PID,使用-c关闭合并,这样可以更细致地查看进程与线程

运行结果:

{ }中的是线程,( )中的是进程PID或者线程TID

可以看到,在Linux上,所有进程的祖先是PID=1的systemd进程,或者称为init进程

结论: PID=1的进程就是init进程,也就是systemd进程,而且这个进程处于用户空间,不是内核空间

systemd进程是由谁创建的呢?

由内核创建,内核完成初始化后,会创建系统中的第一个进程,也就是systemd进程

内核通过位于/init/main.c的kernel_init函数来创建PID=1的用户进程,在这个函数的最后执行的是:

if (!try_to_run_init_process("/sbin/init") ||    !try_to_run_init_process("/etc/init") ||    !try_to_run_init_process("/bin/init") ||    !try_to_run_init_process("/bin/sh"))        return 0;

try_to_run_init_process也定义在/init/main.c中,其内部调用run_init_process函数:

staticinttry_to_run_init_process(constchar *init_filename){int ret;	ret = run_init_process(init_filename);if (ret && ret != -ENOENT) {		pr_err("Starting init: %s exists but couldn't execute it (error %d)\n",		       init_filename, ret);	}return ret;}

run_init_process也定义在/init/main.c中,其内部调用kernel_execve函数:

staticintrun_init_process(constchar *init_filename){const char *const *p;	argv_init[0] = init_filename;	pr_info("Run %s as init process\n", init_filename);	pr_debug("  with arguments:\n");for (p = argv_init; *p; p++)		pr_debug("    %s\n", *p);	pr_debug("  with environment:\n");for (p = envp_init; *p; p++)		pr_debug("    %s\n", *p);return kernel_execve(init_filename, argv_init, envp_init);}

★调用链: kernel_init() → try_to_run_init_process() → run_init_process() → kernel_execve()

注意: kernel_execve()不同于后面OS26.【Linux】进程等待 (下) 和 进程程序替换(上)文章和OS27.【Linux】进程程序替换(下)文章讲的execve系统调用,kernel_execve()是在内核态下执行的,和用户态下进程可以使用的系统调用不一样

原文首发于CSDN,点击阅读全文即可查看

最新文章

随机文章