本帖最后由 yin_wu_qing 于 2024-1-11 22:44 编辑
- 进程是什么?
顾名思义,进程是执行中的程序,即一个程序加载到内存后变成了进程,公式表达如下:进程 = 程序 + 执行
进程是一段执行中的程序,是一个有“生命力”的个体。一个进程除了包含可执行的代码(如代码段),还包含进程的一些活动信息和数据,如用来存放函数形参、局部变量以及返回值的用户栈,用于存放进程相关数据的数据段,用于切换内核中进程的内核栈,以及用于动态分配内存的堆等。进程是用于实现多进程并发执行的一个实体,实现对CPU的虚拟化,让每个进程都认为自己独立拥有一个CPU。实现这个CPU虚拟化的核心技术是上下文切换以及进程调度。
- 操作系统如何描述和抽象一个进程?
进程是操作系统中调度的一个实体,需要对进程所拥有的资源进行抽象,这个抽象形式称为进程控制块(Process Control Block,PCB),本书也称其为进程描述符。进程描述符是用于描述进程运行状况以及控制进程运行所需要的全部信息,是操作系统用来感知进程存在的一个非常重要的数据结构。任何一个操作系统的实现都需要有一个数据结构来描述进程描述符,所以Linux内核采用一个名为task_struct的结构体。task_struct数据结构包含的内容很多,它包含进程所有相关的属性和信息。
- 进程是否有生命周期?
在进程的生命周期内,进程要和内核的很多模块进行交互,如内存管理模块、进程调度模块以及文件系统模块等。因此,它还包含了内存管理,进程调度、文件管理等方面的信息和状态。Linux内核把所有进程的进程描述符task_struct数据结构链接成一个单链表(task_struct->tasks),task_struct数据结构定义在include/linux/sched.h文件中。
- 如何标识一个进程?
在创建时会分配唯一的号码来标识进程,这个号码就是进程标识符(Process Identifier,PID)。PID存放在进程描述符的pid字段中,PID是整数类型。为了循环使用PID,内核使用bitmap机制来管理当前已经分配的PID和空闲的PID,bitmap机制可以保证每个进程创建时都能分配到唯一的PID。
除了PID之外,Linux内核还引入了线程组的概念。一个线程组中所有的线程使用和该线程组中主线程相同的PID,即该组中第一个进程的ID,它会被存入task_struct数据结构的tgid成员中。这与POSIX 1003.1c标准里的规定有关系,一个多线程应用程序中所有的线程必须有相同的PID,这样可以把指定信号发送给组里所有的线程。如一个进程创建之后,只有这个进程,它的PID和线程组ID(Thread Group ID,TGID)是一样的。这个进程创建了一个新的线程之后,新线程有属于自己的PID,但是它的TGID还是指父进程的TGID,因为它和父进程同属一个线程组。
- 进程与进程之间的关系如何?
进程间的关系
成 员 |
描 述 |
real_parent |
指向创建了进程A的描述符,如果进程A的父进程不存在了,则指向进程1(init进程)的描述符 |
parent |
指向进程的当前父进程,通常和real_parent一致 |
children |
所有的子进程都链接成一个链表,这是链表头 |
sibling |
所有兄弟进程都链接成一个链表,链表头在父进程的sibling成员中 |
- Linux操作系统的第0个进程是什么?
系统中所有进程的task_struct数据结构都通过list_head类型的双向链表链接在一起,因此每个进程的task_struct数据结构包含一个list_head类型的tasks成员。这个进程链表的头是init_task进程,也就是所谓的进程0。init_task进程的tasks.prev字段指向链表中最后插入进程的task_struct数据结构的tasks成员。另外,若这个进程下面的有线程组(即PID==TGID),那么线程会添加到线程组的thread_group链表中。
- Linux操作系统的第1个进程是什么?
Linux内核初始化函数start_kernel()在初始化完内核所需要的所有数据结构之后会创建另一个内核线程,这个内核线程就是进程1或init进程。进程1的ID为1,与进程0共享进程所有的数据结构。
进程1会执行kernel_int()函数,它会调用execve()系统调用来装入可执行程序init,最后进程1变成了一个普通进程。这些init程序就是常见的/sbin/init、/bin/init或者/bin/sh等可执行的init以及systemd程序。进程1从内核线程变成普通进程init之后,它的主要作用是根据/etc/inittab文件的内容启动所需要的任务,包括初始化系统配置、启动一个登录对话等。
- 请简述fork()、vfork()和clone()之间的区别。
在Linux内核中,fork()、vfork()、clone()以及创建内核线程的接口函数都是通过调用_do_fork()函数来完成的,只是调用的参数不一样。
//fork()实现
_do_fork(SIGCHLD, 0,0,NULL,NULL,0);
//vfork()实现
_do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0 , 0, NULL, NULL, 0);
//clone()实现
_do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
//内核线程
_do_fork(flags|CLONE_VM |CLONE_UNTRACED,(unsigned long)fn,(unsigned long)arg,NULL,NULL,0);
fork()函数通过系统调用进入Linux内涵,然后通过_do_fork()函数实现。
fork函数只使用SIGCHLD标志位,在子进程终止后发送SIGCHLD信号通知父进程。fork()是重量级调用,为子进程建立了一个基于父进程的完整副本,然后子进程基于此执行。为了减少工作量,子进程采用写时复制技术,只复制父进程的页表,不会复制页面内容。当子进程需要写入新内容时才触发写时复制机制,并为子进程创建一个副本。
fork()函数也有一些缺点,尽管使用了写时复制机制技术,但是它还需要复制父进程的页表,在某些场景下会比较慢,所以有了后来的vfork()原语和clone()原语。
vfork()函数和fork()函数类似,但是vfork()的父进程会一直阻塞,直到子进程调用exit()或者execve()为止。在fork()实现写时复制之前,UNIX系统的设计着很关心fork()之后马上执行execve()所造成的地址空间浪费和效率低下问题,因此设计了vfork()系统调用。
clone()函数通常用于创建用户线程。在Linux内核中没有专门的线程,而是把线程当成普通进程来看待,在内核中还以task_struct数据结构来描述线程,并没有使用特殊的数据结构或者调度算法来描述线程。
clone()函数功能强大,可以传递众多参数,可以有选择地继承父进程的资源,如可以和vfork()一样,与父进程共享一个进程地址空间,从而创建线程;也可以不和父进程共享进程地址空间,甚至可以创建兄弟关系进程。
|