学习《Linux内核设计与实现》, 简单记录一些重要的概念,理清逻辑,如有偏差欢迎批评指正。
1 进程描述符及任务结构
内核将进程的列表存放在任务队列(task list)中维护。任务队列是一个双向循环链表,链表的每一项都是类型为 task_struct (进程描述符 process descriptor)的结构体。
1.1 分配 task_struct
Linux通过slab分配器分配 task_struct 结构, 在栈底(向下生长的栈)或栈顶(向上生长的栈)创建一个 thread_info 结构
struct thread_info {
struct task_struct *task; /* main task structure */
unsigned long flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu;
int preempt_count; /* 0 => preemptable, <0 => BUG */
unsigned long previous_sp; /* sp of previous stack in case
of nested IRQ stacks */
__u8 supervisor_stack[];
};
1.2 进程的状态
系统中的进程必然处于以下五种状态之一, 进程的进程描述符中的status成员必然为下面五种状态之一:
- TASK_RUNNIG : 进程 1.正在执行 2.在运行队列中等待执行
- TASK_INTERRUPTABLE : 进程正在睡眠(被阻塞)
- TASK_UNITERRUPTABLE : 进程正在睡眠, 就算是收到信号也不会被唤醒
- __TASK_TRACED : 进程正在被其他进程跟踪
- __TASK_STOPPED : 进程停止执行,没有投入运行也不能投入运行
1.3 进程上下文
当一个进程执行了系统调用或触发了某个异常, 进程会陷入内核空间。 (事实上进程只用通过这些对内核明确定义的接口才能陷入内核态) 此时我们称内核代表进程执行,(内核)处于进程上下文。
在进程通过系统调用陷入到内核态时, 内核代码执行所使用的栈并非原进程用户空间的栈,而是执行在该进程的内核栈上
1.4 进程家族树
Linux的所有进程都是 PID 为 1 的 init 进程的后代。 Linux内核在系统启动的最后阶段启动 init
进程。 init 进程读取系统的初始化脚本, 执行相应的程序。
系统中的进程必有一个父进程和零活多个子进程, 拥有同一个父进程的子进程之间被称为(sibling), 所有进程的 task_struct 之间通过成员 parent 指针和 children 链表建立起继承关系。
2 进程创建
UNIX 进程创建分为 fork() 和 exec() 两个步骤: fork() 函数拷贝当前进程创建子进程, 子进程与父进程的区别在于PID PPID和一些资源统计量; exec() 函数读取可执行文件载入新的地址空间(在进程的内存空间中介绍)运行。
2.1 写时拷贝
在复制进程时使子进程和父进程共享同一个拷贝, 只有在需要写入时数据才会被复制, 在此之前资源被进程之间只读共享。 采用这种技术后 fork() 的实际开销主要有复制父进程的页表以及创建子进程的 task_struct 。
3 进程与线程
在Linux内核的角度来说没有线程的概念, 它与进程本质的区别就是它与其他线程共享某些资源, 例如地址空间。