进程概念
- 进程就是处于执行期的程序,但进程并不仅仅局限于一段可执行程序代码,还包括其他资源,如:打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,还包括用来存放全局变量的数据段。
- 执行线程,简称线程,是进程中活动的对象,每个线程都拥有一个独立的程序计数器,进程栈和一组进程寄存器,共享进程的地址空间,文件描述符和其他系统资源。内核调度的对象是线程。
进程描述符
task_struct 是操作系统中用于描述进程或任务的基本数据结构,它包含了进程运行所需的所有信息,像是进程的状态、优先级、内存管理信息等,相当于进程在操作系统中的“身份证”和“档案”,便于操作系统对进程进行管理和调度。
struct task_struct {
// 进程基本信息
#ifdef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info; // 线程信息,通常包含底层硬件相关的状态
#endif
unsigned int __state; // 进程的当前状态(如运行、睡眠、停止等)
unsigned int saved_state; // 用于保存进程状态(例如在自旋锁睡眠时)
struct list_head tasks; //进程双向链表
pid_t pid; // 进程的唯一标识符(PID)
pid_t tgid; // 线程组ID(主线程的PID)
struct task_struct __rcu *real_parent; // 指向实际父进程的指针
struct task_struct __rcu *parent; // 指向当前父进程的指针
struct list_head children; // 子进程链表头部
struct list_head sibling; // 用于在父进程的子进程链表中链接兄弟进程
struct task_struct __rcu *group_leader; // 指向进程所在进程组的领头进程的描述符指针
struct mm_struct *mm; //进程地址空间
struct fs_struct *fs; //进程的文件系统目录
struct files_struct *files; //进程打开的文件描述符
// 其他成员
};
进程状态
- 就绪态(Ready)
就绪态表示进程已经具备执行的条件,所有资源都已准备好,但由于 CPU 正在忙于执行其他任务,进程暂时没有获得执行权。
从创建态进入就绪态:进程在创建时进入就绪态,等待被调度。
从运行态回到就绪态:当进程的时间片耗尽或 CPU 被其他更高优先级的进程抢占,进程会回到就绪态,等待下次调度。 - 运行态(Running)
运行态表示进程正在被 CPU 执行。CPU 将按照调度算法分配给该进程时间片,允许其在有限时间内执行。
从就绪态进入运行态:当调度器选中某个进程并为其分配 CPU 时,进程进入运行态。
从运行态转换到其他状态:
如果进程时间片耗尽,会返回到就绪态。
如果进程需要等待某种资源或事件,会进入睡眠态(等待态)。
进程也可以通过收到信号进入暂停态。 - 僵尸态(Zombie)
僵尸态进程(Zombie Process)是指进程已完成执行,但其父进程尚未通过 wait() 或 waitpid() 系统调用获取其退出状态并清理资源。因此,进程仍然保留着一个条目以供父进程读取其退出状态。僵尸态进程不会消耗任何 CPU 资源,但其进程表项仍占用系统资源。
从运行态进入僵尸态:当进程执行完毕并调用 exit() 结束运行后,进入僵尸态,等待父进程回收其资源。
从僵尸态到消失:一旦父进程读取子进程的退出状态,系统会删除僵尸进程条目,释放其占用的资源。 - 可中断睡眠态(Interruptible Sleep)
可中断睡眠态是进程正在等待某一事件的发生,例如等待 I/O 操作完成。此时进程处于睡眠状态,可以通过信号唤醒。
从运行态进入可中断睡眠态:进程调用某些阻塞型系统调用(如 read() 等)等待某种外部事件时进入此状态。
从可中断睡眠态进入就绪态:当外部事件(如 I/O 完成或信号触发)发生时,进程被唤醒,进入就绪态。 - 不可中断睡眠态(Uninterruptible Sleep)
不可中断睡眠态是进程等待某种无法通过信号唤醒的资源。例如,等待硬件操作完成时,进程会进入此状态。此时进程不会响应任何信号,直到所等待的事件发生。
从运行态进入不可中断睡眠态:进程等待特定资源(如硬件 I/O)时进入此状态。
从不可中断睡眠态进入就绪态:
当等待的资源可用时,进程会从不可中断睡眠态醒来,重新进入就绪态。 - 暂停态(Stopped)
暂停态是进程被暂停运行的状态,通常由接收到 SIGSTOP 信号导致。进程停止运行但没有终止,所有的上下文信息都会被保留。
从运行态或就绪态进入暂停态:进程接收到 SIGSTOP 信号后会进入暂停态,暂停其执行。
从暂停态回到就绪态:当进程接收到 SIGCONT 信号时,进程恢复执行并进入就绪态,等待 CPU 调度。
进程链表
全局进程链表
进程链表把所有进程的描述符链接起来。每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev,next分别指向前面和后面的task_struct元素。进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。SET_LINKS和REMOVE_LINKS 宏分别用于从进程链表中插入和删除一个进程描述符。for_each_process 宏用于扫描整个进程链表。
#define for_each_process(p) \
for (p = &init_task; (p = list_entry((p)->tasks.next, \
struct task_struct, tasks)) != &init_task; )
TASK_RUNNING 状态的进程链表
当内核寻找一个新进程在CPU上运行时,必须只考虑可运行进程。linux中建立了多个可运行进程链表,每种进程优先级对应一个不同的链表。每个task_struct描述符包含一个list_head类型的字段run_list。如果进程的优先级等于k,run_list字段把该进程链入优先级为k的可运行进程的链表中。这些链表由prio_array_t数据结构来实现。
struct prio_array {
unsigned int nr_active; //链表中进程描述符的数量
unsigned long bitmap[5]; //优先级位图,当且仅当某个优先级的进程链表不为空时设置相应的标志位
struct list_head queue[140]; //140个优先级链表的头节点
};
enqueue_task(process, array);函数把进程描述符插入某个运行队列的链表
list_add_tail(&process->run_list, &array->queue[process->prio]);
__set_bit(process->prio, array->bitmap);
array->nr_active++;
process->array = array;
父子进程关系链表
通过进程描述符中的children和sibling来实现。children的next指向该进程最新的子进程,prev指向该进程最老的子进程;sibling的next指向它父进程中比它更老的子进程,prev指向它父进程中比它更新的子进程,形成以父进程描述符为头节点的循环双向链表。
pidhash 链表
该链表主要功能为从进程PID找出对应的进程描述符指针,如为kill()系统调用提供服务。为了加速查找,引入了4个散列表。需要4个散列表是因为进程描述符包含了表示不同类型PID的字段,而且每种类型的PID字段需要自己的散列表。
enum pid_type {
PIDTYPE_PID, // 进程的PID
PIDTYPE_TGID, // 线程组领头进程的PID
PIDTYPE_PGID, // 进程组领头进程的PID
PIDTYPE_SID, // 会话领头进程的PID
PIDTYPE_MAX // 类型个数
};
内核初始化期间动态地为4个散列表分配空间,并把它们的地址存入pid_hash数组。
static struct hlist_head *pid_hash[PIDTYPE_MAX];
内核通过一个struct pid的结构来链接各个进程,其定义如下:
struct pid {
int nr; //pid号
struct hlist_node pid_chain; //链接散列链表的下一个和前一个元素
struct list_head pid_list; //每个pid进程链表头
};
等待队列
在进程调度过程中,经常等待某些事情的发生,如等待释放系统资源。等待队列实现了在事件上的条件等待,希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。
等待队列由双向链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头,等待队列头是一个类型为wait_queue_head_t的数据结构
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
其中,spinlock_t lock是一个自旋锁,用于保护等待队列的访问,确保在多处理器或多线程环境下对等待队列的操作是原子的和安全的。struct list_head task_list是一个链表头,用于链接所有在该等待队列中等待的任务。每个等待任务都有一个wait_queue_t类型的结构体,其中包含了指向任务的指针以及其他相关信息,这些结构体通过链表的形式连接在task_list链表中。
struct __wait_queue {
unsigned int flags; //标志信息,例如表示该等待项是否是可中断
void *private; //通常指向与该等待队列项关联的任务,通过这个指针可以找到具体要唤醒的任务
wait_queue_func_t func; //函数指针,指向一个唤醒函数。
struct list_head task_list;//链表节点,连接到 wait_queue_head_t 所管理的链表中
};
typedef struct __wait_queue wait_queue_t;