linux 内核源码 fork 解读
前言,记得某一次开会的时候,学长学姐就说过让我们去看fork源码,结果一直没有时间去看(其实是懒),这不,正好碰上这次开进程的讲座,就在讲座之前看了一波源码,也算是了了一波自己阅读源码的心愿 。
首先我们得基本了解一下,task_struct
与 thread_info
结构是怎么一回事。
1. linux中的PCB的实体(task_struct
)
其实标题已经说的很清楚了。它就是我们常说的进程控制块。
PCB通常记载进程之相关信息,包括:
- 进程状态:可以是new、ready、running、waiting或 blocked等。
- 程序计数器:接着要运行的指令地址。
- CPU寄存器:如累加器、变址寄存器、堆栈指针以及一般用途寄存器、状况代码等, 主要用途在于中断时暂时存储数据,以便稍后继续利用;其数量及类别因计算机体系结构有所差异。
- CPU排班法:优先级、排班队列等指针以及其他参数。
- 存储器管理:如标签页表等。
- 会计信息:如CPU与实际时间之使用数量、时限、账号、工作或进程号码。
- 输入输出状态:配置进程使用I/O设备,如磁带机。
总言之,PCB如其名,内容不脱离各进程相关信息。
内核使用双向循环链表的任务队列来存放进程,使用结构体task_struct来描述进程所有信息。
1 进程描述符task_struct
struct task_struct { }结构体相当大,大约1.7K字节。大概列出一些看看:
struct task_struct
{
struct thread_info thread_info; //必须是第一个元素
//这个是进程的运行时状态,-1代表不可运行,0代表可运行,>0代表已停止。
volatile long state;
/*
flags是进程当前的状态标志,具体的如:
0x00000002表示进程正在被创建; //通过宏定义实现
0x00000004表示进程正准备退出;
0x00000040 表示此进程被fork出,但是并没有执行exec;
0x00000400表示此进程由于其他进程发送相关信号而被杀死 。
*/
unsigned int flags;
void *stack; // 指向内核栈的指针,通过他就可以找到thread_info
//这个是进程号
pid_t pid;
//该结构体描述了虚拟内存的当前状态
struct mm_struct *mm;
......
};
2. thread_info 结构与内核栈
当进程由于中断或系统调用从用户态转换到内核态时,进程所使用的栈也要从用户栈切换到内核栈
内核空间就使用这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。
thread_info 就相当于进程在内核中的一个远方亲戚(内核中的PCB),各自都能通过一个指针指向对方。
struct thread_info {
struct pcb_struct pcb; /* palcode state */
struct task_struct *task; /* main task structure */
unsigned int flags; /* low level flags */
unsigned int ieee_state; /* see fpu.h */
mm_segment_t addr_limit; /* thread address space */
unsigned cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
unsigned int status; /* thread-synchronous flags */
int bpt_nsaved;
unsigned long bpt_addr[2]; /* breakpoint handling */
unsigned int bpt_insn[2];
};
内核处理进程就是通过进程描述符task_struct结构体对象来操作。所以操作进程要获取当前正在运行的进程描述符。通过thread_info的地址就可以找到task_struct地址;在不同的体系结构上计算thread_info的偏移地址不同。
比较两种结构:thread_info更加贴近于系统所处的体系结构,而task_struct比较抽象化,脱离体系而存在。
3. 深入理解 fork
(1)系统调用:
在 Linux 内核中,供用户创建进程的API调用有fork(),vfork(),clone() ,这三个函数的对应的系统调用是 sys_fork()、sys_clone()、sys_vfork()。
这三个函数都是通过调用内核函数 do_fork() 来实现的,而现代linux内核do_fork()又调用了_do_fork( )函数,所以重点来了,我们只需要把关注点放在_do_fork( )函数即可
(2)_do_fork() 函数
long _do_fork(unsigned long clone_flags,
//从clone_flag参数标志来表明进程创建的方式
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
1. 检查参数
2. strct task_struct *p;
3. p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls);
// 将进程插入运行队列,此时状态为TASK_RUNNING
4.wake_up_new_task(p);
5. return ? ;
}
(2)copy_process( )函数
/*/*
* This creates a new process as a copy of the old one,
* but does not actually start it yet.
* 根据clone_flags标志拷贝寄存器,以及其他进程环境
* It copies the registers, and all the appropriate(适当)
* parts of the process environment (as per the clone
* flags). The actual kick-off is left to the caller.
* 搞好的这个新的进程的启动由调用者完成启动
*/
task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{ struct task_struct *p;
//创建进程内核栈和进程描述符
p = dup_task_struct(current);
//得到的进程与父进程内容完全一致,初始化新创建进程
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
//将寄存器%ax置为0,也是子进程pid返回0的原因
pid = alloc_pid(p->nsproxy->pid_ns_for_children); //分配新的 Pid
……
return p;
}
(3)dup_task_struct()函数
/*为新进程创建新的内核堆栈(hread_info)和PCB(task_struct)结构。*/
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int node = tsk_fork_get_node(orig);
//创建进程描述符对象
tsk = alloc_task_struct_node(node);
//创建进程内核栈 thread_info
ti = alloc_thread_info_node(tsk, node);
//使子进程描述符和父进程一致,为什么会一直
err = arch_dup_task_struct(tsk, orig);
//进程描述符stack指向thread_info
tsk->stack = ti;
//使子进程thread_info内容与父进程一致但task指向子进程task_struct
setup_thread_stack(tsk, orig);
return tsk;
}