1. 课程学习心得体会
通过本门课程Linux操作系统分析课程的学习,使我加深了对操作系统相关知识的理解,比如进程管理、中断机制、系统调用、文件系统。作为一个软件开发人员我觉得进程相关对知识对于工程师来说特别重要,比如面试中必问的对于操作系统进程的理解,进程和线程的区别?接下来我主要总结了Linux进程相关的知识。
2. Linux进程管理总结
进程表示:在 Linux 内核内,进程是由相当大的一个称为 task_struct
的结构表示的。此结构包含所有表示此进程所必需的数据,此外,还包含了大量的其他数据用来统计(accounting)和维护与其他进程的关系(父和子)。其中可以看到,进程执行的状态、堆栈、一组标志、父进程、执行的线程(可以有很多)以及开放文件。
state
变量是一些表明任务状态的比特位。最常见的状态有:TASK_RUNNING
表示进程正在运行,或是排在运行队列中正要运行;TASK_INTERRUPTIBLE
表示进程正在休眠、TASK_UNINTERRUPTIBLE
表示进程正在休眠但不能叫醒;TASK_STOPPED
表示进程停止等等。
flags
定义了很多指示符,表明进程是否正在被创建(PF_STARTING
)或退出(PF_EXITING
),或是进程当前是否在分配内存(PF_MEMALLOC
)。可执行程序的名称(不包含路径)占用 comm
(命令)字段。
每个进程都会被赋予优先级(称为 static_prio
),但进程的实际优先级是基于加载以及其他几个因素动态决定的。优先级值越低,实际的优先级越高。
tasks
字段提供了链接列表的能力。它包含一个 prev
指针(指向前一个任务)和一个 next
指针(指向下一个任务)。
进程的地址空间由 mm
和 active_mm
字段表示。mm
代表的是进程的内存描述符,而 active_mm
则是前一个进程的内存描述符(为改进上下文切换时间的一种优化)。
struct task_struct { volatile long state; void *stack; unsigned int flags; int prio, static_prio; struct list_head tasks; struct mm_struct *mm, *active_mm; pid_t pid; pid_t tgid; struct task_struct *real_parent; char comm[TASK_COMM_LEN]; struct thread_struct thread; struct files_struct *files; ... };
进程创建:用户空间任务和内核任务的底层机制是一致的,因为二者最终都会依赖于一个名为 do_fork
的函数来创建新进程。在创建内核线程时,内核会调用一个名为 kernel_thread
的函数,此函数执行某些初始化后会调用 do_fork
。
do_fork
函数首先调用 alloc_pidmap
,该调用会分配一个新的 PID。接下来,do_fork
检查调试器是否在跟踪父进程。如果是,在 clone_flags
内设置 CLONE_PTRACE
标志以做好执行 fork 操作的准备。之后 do_fork
函数还会调用 copy_process
,向其传递这些标志、堆栈、注册表、父进程以及最新分配的 PID。
新的进程在 copy_process
函数内作为父进程的一个副本创建。此函数能执行除启动进程之外的所有操作,启动进程在之后进行处理。copy_process
内的第一步是验证 CLONE
标志以确保这些标志是一致的。如果不一致,就会返回 EINVAL
错误。接下来,询问 Linux Security Module (LSM) 看当前任务是否可以创建一个新任务。
接下来,调用 dup_task_struct
函数,这会分配一个新 task_struct
并将当前进程的描述符复制到其内。在新的线程堆栈设置好后,一些状态信息也会被初始化,并且会将控制返回给 copy_process
。控制回到 copy_process
后,除了其他几个限制和安全检查之外,还会执行一些常规管理,包括在新 task_struct
上的各种初始化。之后,会调用一系列复制函数来复制此进程的各个方面,比如复制开放文件描述符(copy_files
)、复制符号信息(copy_sighand
和 copy_signal
)、复制进程内存(copy_mm
)以及最终复制线程(copy_thread
)。
进程调度:Linux 调度程序维护了针对每个优先级别的一组列表,其中保存了 task_struct
引用。任务通过 schedule
函数调用,它根据加载及进程执行历史决定最佳进程。
进程状态:在Linux系统中,一个进程被创建之后,在系统中可以有下面5种状态。进程的当前状态记录在进程控制块的state成员中。
就绪状态和运行状态
就绪状态的状态标志state的值为TASK_RUNNING。此时,程序已被挂入运行队列,处于准备运行状态。一旦获得处理器使用权,即可进入运行状态。
当进程获得处理器而运行时 ,state的值仍然为TASK_RUNNING,并不发生改变;但Linux会把一个专门用来指向当前运行任务的指针current指向它,以表示它是一个正在运行的进程。
可中断等待状态
状态标志state的值为TASK_INTERRUPTIBL。此时,由于进程未获得它所申请的资源而处在等待状态。一旦资源有效或者有唤醒信号,进程会立即结束等待而进入就绪状态。
不可中断等待状态
状态标志state的值为TASK_UNINTERRUPTIBL。此时,进程也处于等待资源状态。一旦资源有效,进程会立即进入就绪状态。这个等待状态与可中断等待状态的区别在于:处于TASK_UNINTERRUPTIBL状态的进程不能被信号量或者中断所唤醒,只有当它申请的资源有效时才能被唤醒。
这个状态被应用在内核中某些场景中,比如当进程需要对磁盘进行读写,而此刻正在DMA中进行着数据到内存的拷贝,如果这时进程休眠被打断(比如强制退出信号)那么很可能会出现问题,所以这时进程就会处于不可被打断的状态下。
停止状态
状态标志state的值为TASK_STOPPED。当进程收到一个SIGSTOP信号后,就由运行状态进入停止状态,当受到一个SIGCONT信号时,又会恢复运行状态。这种状态主要用于程序的调试,又被叫做“暂停状态”、“挂起状态”。
中止状态
状态标志state的值为TASK_DEAD。进程因某种原因而中止运行,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外,并且系统对它不再予以理睬,所以这种状态也叫做“僵死状态”,进程成为僵尸进程。
在进程的整个生命周期中,它可在5种状态之间转换。Linux进程5种状态之间的转换关系如下图所示:
进程销毁:进程销毁可以通过几个事件驱动 — 通过正常的进程结束、通过信号或是通过对 exit
函数的调用。
do_exit
的目的是将所有对当前进程的引用从操作系统删除(针对所有没有共享的资源)。销毁的过程先要通过设置 PF_EXITING
标志来表明进程正在退出。内核的其他方面会利用它来避免在进程被删除时还试图处理此进程。将进程从它在其生命期间获得的各种资源分离开来是通过一系列调用实现的,比如 exit_mm
(删除内存页)和 exit_keys
(释放线程会话和进程安全键)。do_exit
函数执行释放进程所需的各种统计,这之后,通过调用 exit_notify
执行一系列通知(比如,告知父进程其子进程正在退出)。最后,进程状态被更改为 PF_DEAD
,并且还会调用 schedule
函数来选择一个将要执行的新进程。请注意,如果对父进程的通知是必需的(或进程正在被跟踪),那么任务将不会彻底消失。如果无需任何通知,就可以调用 release_task
来实际收回由进程使用的那部分内存。