Linux 进程
参考:
-
「linux操作系统」进程的切换与控制·到底有啥关系? - 知乎 (zhihu.com),Linux进程解析_deep_explore的博客-CSDN博客,腾讯面试:进程的那些数据结构 - 知乎 (zhihu.com),如何在Linux下的进行多进程编程(初步) - 知乎 (zhihu.com),彻底搞定面试官,linux的进程里面的一些细节 - 知乎 (zhihu.com),操作系统进程的概念,进程的状态及状态转换,进程控制程小智的博客-CSDN博客进程的状态及其转换。
-
Linux 进程详解 - 程序员大本营 (pianshen.com)。Linux进程基础教程详解Linux脚本之家 (jb51.net)。
-
【Linux】Linux进程的创建与管理Yngz_Miao的博客-CSDN博客linux 创建进程。
-
《Linux System Prorgrammin》,Linux系统编程 _ 中文版 _ by _ 哈工大(翻译)-第五章-进程管理。
-
Linux 操作系统 C 语言编程入门。
进程基础概念
程序与进程
通俗的讲程序是一个包含可以执行代码的文件,是一个静态的文件。而进程是一个开始执行但是还没有结束的程序的实例。就是可执行文 件的具体实现。一个程序可能有许多进程,而每一个进程又可以有许多子进程。
进程状态
创建态:进程正在被创建,尚未转到就绪态。
就绪态:进程获得了除处理机以外的一切所需资源,一旦得到处理机便可立即运行。
状态特点:处理机(或者理解为调度器)资源:只缺处理机。资源获得:已获得所需资源。当获得处理机时:立即运行。
状态转换:就绪态——>运行态:处于就绪态的进程被调度后,获得处理机资源,于是进程由就绪态切换为运行态。
运行态:进程正在处理机上运行;对于单处理机,同一时刻只有一个进程处于运行态。
状态转换:运行态——>就绪态:情况1:处于运行态的进程在时间片用完后,不得不让出处理机,进而转换为就绪态。情况2:在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程序将正在执行的进程转换为就绪态,让更高优先级的进程执行。
状态转换:运行态——>阻塞态(主动行为):进程请求某一资源(如外设)的使用或等待某一事件的发生(如I/O操作的完成)时,它就从运行态转换为阻塞态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的,由用户态程序调用操作系统内核过程的形式。
阻塞态:又称等待态,进程正在等待某一事件而暂停运行/休眠,如等待某资源或IO完成,即使处理机空闲,该进程也不能运行。
状态特点:处理机(或者理解为调度器)资源:可能缺;也可能不缺。资源获得:等待某资源可用或等待一件事情完成。当获得处理机时:即使处理机空闲,当等待的事情没有完成,仍无法运行。
状态转换:阻塞态——>就绪态(被动行为,需要其他相关进程的协助):进程等待的事件到来,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞态转换为就绪态。
终止态:进程正从系统中消失,可能是进程正常结束或其他原因退出运行。
对应到 Linux 内核中,各个进程状态的标志:
TASK_RUNNING
说明进程已经准备好了,就看操作系统给不给分时间片在 CPU 上执行了,进程获得了时间片,就是执行状态,不分配时间片就是就绪状态。代表状态的字段又不用变。
其实
TASK_RUNING
这个字段既对应了进程的就绪态又对应了进程的运行态。只有在该状态的进程才可能在 CPU上运行。而同 一时刻可能有多个进程处于可执行状态,这些进程的 task_struct结构(进程控制块)被放入对应 CPU的可执行队列中(一个进程最多只能出现在一个 CPU的可执行队列中)。进程调度器的任务就是从各个 CPU的可执行队列中分别选择一个进程在该 CPU 上运行。
只要可执行队列不为空,其对应的 CPU就不能偷懒,就要执行其中某个进程。一般称此时的 CPU“忙碌”。对应的, CPU“空闲”就是指其对应的可执行队列为空,以致于 CPU无事可做。
很多操作系统教科书将正在 CPU上执行的进程定义为 RUNNING状态、而将可执行但是尚未被调度执行的进程定义为 READY状态,这两种状态在 linux下统一为 TASK_RUNNING状态。
TASK_INTERRUPTIBLE
和TASK_UNINTERRUPTIBLE
是两种睡眠状态,对应上面的阻塞状态。TASK_INTERRUPTIBLE
可以再被信号唤醒,TASK_UNINTERRUPTIBLE
不可被信号唤醒。
TASK_INTERRUPTIBLE,可中断的睡眠状态。处于这个状态的进程因为等待某某事件的发生(比如等待 socket连接、等待信号量),而被挂起。这些进程的 task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他 进程触发),对应的等待队列中的一个或多个进程将被唤醒。通过 ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于 TASK_INTERRUPTIBLE状态(除非机器的负载很高)。
TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。与 TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是 CPU不响应外部硬件的中断,而是指进程不响应异步信号。即 kill -9 无法 关掉/杀死 这种进程。TASK_UNINTERRUPTIBLE 状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。
在进程对某些硬件进行操作时(比如进程调用 read 系统调用对某个设备文件进行读操作,而 read 系统调用最终执行到对应设备驱动的代码,并与对应的物 理设备进行交互),可能需要使用 TASK_UNINTERRUPTIBLE 状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的 状态。这种情况下的 TASK_UNINTERRUPTIBLE 状态总是非常短暂的,通过 ps 命令基本上不可能捕捉到。Linux系统中也存在容易捕捉的 TASK_UNINTERRUPTIBLE状态。执行 vfork系统调用后,父进程将进入TASK_UNINTERRUPTIBLE状态,直到子进程调用 exit或 exec。
TASK_STOPPED
是在进程收到 SIGSTOP 以及 SIGTTIN 等信号的状态,你 Linux 进程运行起来按 Ctrl + z 后进程就是这个状态。
向进程 发送一个 SIGSTOP信号,它就会因响应该信号而进入 TASK_STOPPED状态(除非该进程本身处于 TASK_UNINTERRUPTIBLE状态而不响应信号)。( SIGSTOP与 SIGKILL信号一样,是非常强制的。不允许用户进程通过 signal系列的系统调用重新设置对应的信号处理函数。)
向进程发送一个 SIGCONT信号,可以让其从 TASK_STOPPED状态恢复到 TASK_RUNNING状态。
TASK_TRACED
是进程被监视的状态。
当进程正在被跟踪时,它处于 TASK_TRACED这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在 gdb中 对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于 TASK_TRACED状态。而在其他时候,被跟踪的进程还是处于前面提到的那些状态。
TASK_STOPPED和 TASK_TRACED状态判断。对于进程本身来说, TASK_STOPPED和 TASK_TRACED状态很类似,都是表示进程暂停下来。而 TASK_TRACED状态相当于在 TASK_STOPPED之上多了一层保护,处于 TASK_TRACED状态的进程不能响应 SIGCONT信号而 被唤醒。只能等到调试进程通过 ptrace系统调用执行 PTRACE_CONT、 PTRACE_DETACH等操作(通过 ptrace系统调用的参数指定 操作),或调试进程退出,被调试的进程才能恢复 TASK_RUNNING状态。
TASK_DEAD
-EXIT_ZOMBIE
,退出状态,进程成为僵尸进程。EXIT_DEAD
是最终状态,进入这个状态代表进程要从系统中删除了EXIT_ZOMBIE
是EXIT_DEAD
的前一个状态,这个时候进程已经终止,但父进程还没有用wait()
等系统调用来获取他的终止信息,这个状态的进程叫做僵尸进程。这个状态 kill 命令是杀不死的,你们可以想以下应该怎样清楚僵尸进程,以及怎样避免僵尸进程的存在。关于 退出 相关的 进程状态(上面四个),更多可详见 Linux进程解析_deep_explore的博客-CSDN博客。
Linux 进程状态转换示意图:
系统进程常见的 STAT 代码:
就绪队列与阻塞队列:
就绪队列:系统中处于就绪状态的进程可能有多个,通常把它们排成一个队列。只要就绪队列不空,CPU就总是可以调度进程运行,保持繁忙,这与就绪进程的数目没有关系;除非就绪队列为空,此时CPU进入等待态,CPU效率下降。
阻塞队列:系统通常将处于阻塞态的进程也排成一个队列,甚至根据阻塞原因不同,设置多个阻塞队列。
进程的构成
引自 进程的那些数据结构 - 知乎 (zhihu.com),Linux下的task_struct结构体 - 百度文库 (baidu.com)。
进程一般由以下几个部分组成:
进程控制块(PCB):每个进程在创建时, 系统都会为进程创建一个相应的 PCB。PCB 是进程存在的唯一标志。
创建进程实质就是创建进程的 PCB。PCB 要能展示进程身份和关系,标记任务状态,标记权限,帮助任务调度等等。
Linux 内核中是把进程和线程统一当作任务来实现的,Linux 内核的 进程控制块 是
task_struct
结构体,里面包含有:
The identifier of the process (a process identifier , or PID) ;(进程的标识自身的唯一标识符 PID)
Register values for the process including, notably, the program counter and stack pointer values for the process;(进程调度时候退出时间片(保存现场)与进入时间片(恢复现场)时候用到的寄存器值包括栈指针 SP、程序计数器 PC 等等)
The address space for the process;(进程的地址空间)
Priority (in which higher priority process gets first preference. eg., nice value on Unix operating systems);(优先级)
Process accounting information, such as when the process was last run, how much CPU time it has accumulated, etc;
Pointer to the next PCB i.e. pointer to the PCB of the next process to run;
I/O Information (i.e. I/O devices allocated to this process, list of opened files, etc).
pid_t pid; // 展示自己进程的 id pid_t tgid; // 进程主线程的id struct task_struct *group_leader; // 指向主线程地址每个进程都会创建一个主线程,所以如果只是单独一个进程,以及进程默认创建的主线程,那么
pid
和tgid
都会是自己。如果是一个进程创建的子线程,那么pid
就是自己的id
,tgid
就指向进程主线程的 id。 struct task_struct __rcu * real_parent; struct task_struct __rcu * parent; // 指向父进程 struct list_head children; // 父进程的所有子进程都在子进程链表中,这里指向链表的头部。 struct list_head sibling; // 连接兄弟进程进程是一个树状的结构(使用链表组成的树),除了 0 号进程外,所有的进程都是由父进程创建的,所以对父进程的操作很容易就会影响到子进程。所以进程的数据结构中自然要显示出进程有哪些父子进程以及兄弟进程。
程序段:程序段是进程中能被进程调度程序调度到 CPU 上执行的程序代码段。
数据段:可以是进程对应程序加工的原始数据,也可以是程序执行时产生的中间 结果或结果数据。
进程控制
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。简而言之,进程控制就是为了实现进程状态转换。
一般 进程控制 的 程序段 是 “原子操作” 的,执行过程 期间不允许被中断;它使用 “关中断指令” 和 “开中断指令” 这两个特权指令实现原子性。
进程创建和终止
进程控制之进程创建和终止 相关的概念。
引起进程创建的事件
用户登陆:分时系统中,用户登陆成功,系统会为其建立一个新的进程。
作业调度:多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程。
提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求。启动程序执行都会创建一个新进程。
应用请求:由用户进程主动请求创建一个子进程。
操作系统创建新进程的过程
Step1:为新进程分配一个唯一的进程标识号,并申请一个空白PCB(PCB是有限的),若PCB申请失败,则创建失败。
Step2:为进程分配所需资源,如文件、内存、I/O设备和CPU时间等。这些资源从操作系统获得,或从其父进程获得。如果资源不足(如内存),则此时并不是创建失败,而是处于创建态,等待内存资源。
Step3:初始化PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等。
Step4:若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。
父进程创建子进程
允许一个进程创建另一个进程,此时创建者称为父进程,被创建的进程称为子进程。子进程可以继承父进程所拥有的资源;当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程;当父进程被撤销时,通常也会同时撤销其所有的子进程。父进程和子进程共享一部分资源,但不能共享虚拟地址空间,在创建子进程时,会为子进程分配资源,如虚拟地址空间等。
父进程与子进程当然可以并发执行。进程控制块(PCB)是进程存在的唯一标志,每个进程都有自己的PCB。父进程与子进程不能同时使用同一临界资源,临界资源一次只能被一个进程使用(临界资源就是加了锁机制,只能被互斥地访问)。
Linux 系统创建进程都是由已存在的进程创建的(除了0号进程),被创建的进程叫做子进程,创建子进程的进程就做父进程。Linux 进程串起来是一颗树的结构。
引起进程终止的事件
正常结束:表示进程的任务已完成并准备退出运行。
异常结束:表示进程在运行时,发生了某种异常事件,使程序无法继续运行,如存储区越界、保护错、非法指令、特权指令错、运行超时、算术运算错、I/O故障等。
外界干预:指进程应外界的请求而终止运行,如操作员或操作系统干预、父进程请求和父进程终止。
操作系统终止进程的过程
Step1:根据被终止进程的标识符(PID),检索出该进程的PCB,从中读出该进程的状态。
Step2:若被终止的进程正处于运行态,应立即终止该进程的运行,将处理机资源分配给其他进程。
Step3:若该进程还有子孙进程,则应当将其所有子孙进程终止。
Step4:将该进程所拥有的全部资源,或归还给其父进程,或归还给操作系统。
Step5:该PCB从所在队列中删除。
查看和释义各个进程
我们在 Linux 系统上通过
ps - ef
命令查看系统目前的进程。
UID 就是用户的标识符(通过 root 用户创建的进程 UID 就是 root,如果我自己创建的话就应该是我的用户名。
PID 就表示的是当前进程的 id。
PPID 就表示当前进程的父进程 id。
通过 0 号进程创建 1 号进程和 2 号进程,然后通过 1 号进程去创建用户态进程,再通过 2 号进程创建内核态进程,就生成了 Linux 进程树。
0号进程
:在内核初始化的过程中,会先通过指令 struct task_struct init_task = INIT_TASK(init_task) 创建 0 号进程。这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程。是进程列表的第一个。但是这个进程不是实际意义上的进程,类似与链表头。所以虽然 0 号进程是在内核态创建的,但不能说 0 号进程是内核态的第一个进程,反而要说 2 号进程是内核态的第一个进程。
1号进程
:通过调用指令 kernel_thread(kernel_init, NULL, CLONE_FS) 从内核态切换到用户态来创建的,1号进程是所有用户态的祖先。进程1也叫做init进程,它是内核初始化时创建的第2个内核线程,其运行代码为内核函数init()。只要系统不结束,init进程就永不中止,它负责创建和监控操作系统外层所有进程的活动。
2号进程
:通过调用指令 kernel_thread(kthreadd, NULL, ClONE_FS | CLONE_FILES) 来创建,2号进程负责所有内核态的进程的调度和管理,是内核态所有进程的祖先。(注意,内核态不区分线程和进程,所以说进程和线程都可以,都是任务)。
pstree
命令来显示整个进程树, Linux基础命令---显示进程树pstree_weixin_34023863的博客-CSDN博客。