进程属性
今天开展第二部分的内容学习,关于进程属性,由于白天比较忙,能够用来深入学习的时间,只能草草了事了T_T
进程描述符
进程是操作系统调度的一个实体,需要 对进程所拥有的资源进行抽象,这个抽象的形式称为进程控制块(PCB, Porcess Control Block), 也叫做进程描述符。进程描述符主要用于描述进程运行状态及控制进程运行所需的全部信息,是操作系统用来感知进程存在的一个非常重要的数据结构。在Linux内核中,叫做task_struct结构体。task_struct包含很多内容,包含了进程所有相关的属性和信息。(进程会和内核中的内存管理模块、进度调度模块以及文件系统模块等发生交付,因此会包含相关模块信息和状态)
进程生命周期
进程的生命周期包含4个状态,从创建进程开始,随后有就绪状态、执行状态、阻塞状态以及终止状态
对应的Linux内核也定义了5种状态,
TASK_RUUNING: 包含了就绪状态和运行状态
TASK_INTERRUPTIBLE: 可中断睡眠状态
TASK_UNINTERRUPTIBLE: 不可中断状态
EXIT_ZOMBIE:僵尸状态,进程已经消亡了,但是task_struct数据结构还没有释放
__TASK_STOPPED:进程终止
进程标识
在创建时会分配唯一的ID来标识进程,这个ID就是进程标识符(Process Identifier,PID),由于Linux系统不特别区分进程和线程,所以进程和线程都会通过PID进行标识,会获取到PID号
PID是一个比较好理解的概念,只要是学过Linux的,基本都知道PID,并且知道使用ps命令去查看进程的pid号
在十几年的运维生涯中踩到过一个pid的坑。在Linux Kernel版本3系列,在虚拟机上的pid max值一般默认是被设置成了65535。 从上面描述的PID的概念来看,PID是一个顺序使用的过程,一旦PID到达了65535,之后就会从头开始见缝插针,发现有之前用过已经被回收了的,就会重新复用。在实际的业务场景中,有一些写的不太好的系统代码就会导致操作系统异常。举个极端的例子,如果一个java进程启动后配置了会fork 65535个线程出来,结果会怎么样?结果就是PID被耗尽,导致新的线程或操作系统其他进程无法打开,形成假死状态,此时使用df -h, free等命令都会报错“-bash: fork: Cannot Allocate Memory", 看上去像是内存不足,但千万别被迷惑了,实际就是PID满了, 因为Linux一切皆文件,执行这些命令都需要占用一个文件描述符(fd)。此时如果你还有终端在虚拟机上,那就谢天谢地把,直接用echo "655350" > /proc/sys/kernel/pid_max, 直接修改当前的pidmax值来扩充pid的数量。如果没有终端,那基本就没戏,建议重启,因为ssh登录也是需要占用fd的,当然,如果业务系统比较重要,不能重启,那就看运气吧。不停的尝试ssh。
进程上下文
可执行程序代码是进程的重要组成部分。
从一个可执行文件载入到进程的地址空间执行,程序一般在用户空间执行,当程序执行了系统调用或者出发了某个异常,它就会陷入内核空间,此时,称内核“代表进程执行”并处于进程上下文中。在从内核执行完退出时,程序恢复在用户空间会继续执行。
进程家族关系
Linux内核维护了进程间的家族关系
所有进程都是PID为1的init进程后代。系统中每个进程必须有一个父进程,每个进程可以拥有多个子进程。拥有同一个父进程的进程称为兄弟。进程间的关系也放在PCB中。
系统中所有进程的task_struct数据结构都通过list_head类型的双向链表链接在一起,因此每个task_struct数据结构都包含一个list_head类型的tasks成员。
特殊进程0和1
linux内核在启动时会有一个init_task进程,他是系统中所有进程的祖先,叫做0号进程或者idle进程或者swapper进程,pid为0,他是在内核代码之中。
Linux内核初始化函数start_kernel()在完成初始化所需要的数据结构之后,会创建另外一个内核线程,叫做1号进程或者init进程,pid为1。0号进程和1号进程共享进程所有的数据结构。
创建完init进程之后,进程0会执行cpu_idle()函数,当某个cpu的就绪队列无需要执行的进程,处于闲置状态,会通过调度器去执行进程0,并让该cpu进入空闲状态,即此时通过top或者mpstat或者iostat看到的cpu的使用率为0
进程1会执行kernel_init()函数,调用execve()系统调用来装入可执行程序init,最后进程1变成了一个普通进程。这些init进程就是常见的/sbin/init, /bin/init、/bin/sh等可执行init以及systemd。