前言,记得某一次开会的时候,学长学姐就说过让我们去看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;
......
};
以上都是瞎逼逼。源码地址在:https://elixir.bootlin.com/linux/v5.3.1/source/include/linux/sched.h#L637
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 的偏移地址不同。
3. 深入理解 fork
(1)系统调用:
系统调用 :Linux 系统中,用户空间通过向内核空间发出 Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。对于每个系统调用都会有一个对应的系统调用号。
Syscall是通过中断方式实现的,ARM平台上通过swi中断来实现系统调用,实现从用户态切换到内核态,发送软中断swi时,从中断向量表中查看跳转代码,其中异常向量表定义在文件https://elixir.bootlin.com/linux/v5.3.1/source/arch/arm/kernel/entry-armv.S(汇编语言文件)。当执行系统调用时会根据系统调用号从系统调用表中来查看目标函数的入口地址,在calls.S文件中声明了入口地址信息。
更加具体的见:http://gityuan.com/2016/05/21/syscall/
在 Linux 内核中,供用户创建进程的API调用有fork(),vfork(),clone() ,这三个函数的对应的系统调用是 sys_fork()、sys_clone()、sys_vfork()。
这三个函数都是通过调用内核函数 do_fork() 来实现的,而现代 linux 内核 do_fork() 又调用了_do_fork( ) 函数,所以重点来了,我们只需要把关注点放在 _do_fork( ) 函数即可
fork、vfork以及 clone 的区别
系统调用 | 详细描述 |
---|---|
fork | fork创造的子进程是父进程的完整副本,复制了父进程的资源: task_struct,打开文件表,信号,命名空间虚拟地址空间(包括堆栈等)等。(写时复制) |
vfork | vfork创建的子进程与父进程共享虚拟地址空间,所以子进程的改变会影响父进程中的数据。vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。 |
clone | 系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程。具体要复制哪些资源给子进程,由参数列表中的clone_flags决决定。 |
fork,vfork 都是由 clone 实现的。底层由 _do_fork() 函数支持。
内核中好像已经没有vfork 了。(反正在这个网站上没有搜出来关键字)
写时复制
先来看一下我们运行一个 a.out 程序具体是如何实现的:
深入理解Linux内核之Hello world 到底发生了什么?
在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个
。
当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
-
Linux系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell 运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。(运行镜像的一种复制)
-
子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。
-
通过将虛拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件的内容。
-
execve负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。
内核线程与用户线程(略)
(2) _do_fork() 函数
#ifndef CONFIG_HAVE_COPY_THREAD_TLS
/* For compatibility with architectures that call do_fork directly rather than
* using the syscall entry points below. */
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct kernel_clone_args args = {
.flags = (clone_flags & ~CSIGNAL),
.pidfd = parent_tidptr,
.child_tid = child_tidptr,
.parent_tid = parent_tidptr,
.exit_signal = (clone_flags & CSIGNAL),
.stack = stack_start,
.stack_size = stack_size,
};
if (!legacy_clone_args_valid(&args))
//1.查找 pid 位图,为子进程分配新的 pid
return -EINVAL;
return _do_fork(&args);
}
long _do_fork(struct kernel_clone_args *args)
{
u64 clone_flags = args->flags;
struct completion vfork;
struct pid *pid;
struct task_struct *p;
int trace = 0;
long nr;
//2.关于进程追踪的设置
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if (args->exit_signal != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
//3.复制进程描述符
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, args->parent_tid);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
//4.将子进程放在运行队列中父进程的前面
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
//5.如果是 vfork() 的话父进程插入等待队列,挂起父进程直到子进程释放自己的内存地址空间
//(直到子进程结束或者执行新的程序)
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
return nr;
}
(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;
//1.复制进程内核栈(thread_info)和进程描述符结构
p = dup_task_struct(current);
//2.一些相关处理
。。。 shm_init_task(p);
security_task_alloc(p, clone_flags);
//3.复制父进程的所有数据结构
copy_semundo(clone_flags, p);
copy_files(clone_flags, p);
copy_fs(clone_flags, p);
copy_sighand(clone_flags, p);
copy_signal(clone_flags, p);
copy_mm(clone_flags, p);
copy_namespaces(clone_flags, p);
copy_io(clone_flags, p);
//4.初始化子进程的内核栈。将寄存器%eax置为0,也是子进程pid返回0的原因
copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
args->tls);
stackleak_task_init(p);
pid = alloc_pid(p->nsproxy->pid_ns_for_children); //分配新的 Pid
//设置子进程的 pid
p->pid = pid_nr(pid);
//如果是创建线程
if (clone_flags & CLONE_THREAD)
{
p->exit_signal = -1;
p->group_leader = current->group_leader;
//tgid 是当前线程组的 id
p->tgid = current->tgid;
}
else
{
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p;
p->tgid = p->pid;
}
if (clone_flags & (CLONE_PARENT | CLONE_THREAD))
{
//如果是创建线程,那么同一线程组内的所有线程共享进程空间
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
}
else
{
//如果是创建进程,当前进程就是子进程的父进程
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
}
attach_pid(p, PIDTYPE_PID);
nr_threads++;
//返回被创建的 task 结构体指针
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;
}