学号最后三位编号:008
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
Linux进程描述符也被称为进程控制块PCB,进程描述符都是task_struct类型的数据结构,它的字段包含了一个与进程相关的所有信息,不仅包含了很多进程属性的字段,而且一些字段还包括了指向其它数据结构的指针。
task_struct结构中主要包含以下内容:
- 状态信息:如就绪、执行等状态
- 链接信息:用来描述进程之间的家庭关系,例如指向父进程、子进程、兄弟进程等PCB的指针
- 各种标识符:如进程标识符、用户及组标识符等
- 时间和定时器信息:进程使用CPU时间的统计等
- 调度信息:调度策略、进程优先级、剩余时间片大小等
- 处理机环境信息:处理器的各种寄存器以及堆栈情况等
- 虚拟内存信息:描述每个进程所拥有的地址空间
- 文件系统信息:记录进程使用文件的情况
task_struct结构中部分成员变量如下所示:
struct task_struct
{
volatile long state; // 描述进程状态
pid_t pid; // 进程标识符(PID)
void *stack; // 进程内核栈
unsigned int policy; // 描述进程调度策略
struct list_head tasks; // 用于创建进程链表
......
};
do_fork()函数负责处理clone()、fork()和vfork()系统调用,函数原型如下所示:
long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr)
参数说明如下所示:
clone_flags | 通过clone标志有选择的对父进程的资源进行复制,fork,vfork和clone系统调用就是根据flag标志不同加以区分的 |
---|---|
stack_start | 子进程用户态堆栈的地址 |
stack_size | 未被使用,通常被赋值为0 |
parent_tidptr | 父进程在用户态下pid的地址,只有在CLONE_PARENT_SETTID标志被设定时才有意义 |
child_tidptr | 子进程在用户态下pid的地址,也是在CLONE_CHILD_SETTID标志被设定时有意义 |
do_fork()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其它内核数据结构。
do_fork()函数的源代码如下所示:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p; // 定义一个task_struct类型的指针p
int trace = 0;
long nr;
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
/*复制进程描述符,copy_process()的返回值是一个 task_struct 类型的指针*/
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
/*得到新创建的进程的pid信息*/
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
/*如果调用的 vfork()方法,初始化 vfork 完成处理信息*/
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork); // 初始化完成变量
get_task_struct(p);
}
/*将子进程加入到调度器中,为其分配 CPU,准备执行 */
wake_up_new_task(p);
if (unlikely(trace))
ptrace_event_pid(trace, pid);
/*如果是 vfork,将父进程加入至等待队列,等待子进程完成*/
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr; // 返回子进程的进程描述符
}
copy_process()函数的部分源代码如下所示:
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p; // 定义一个task_struct类型的指针p
//检查clone_flags标志的设置是否正确
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
if (clone_flags & CLONE_SIGHAND) {
if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
(task_active_pid_ns(current) !=
current->nsproxy->pid_ns_for_children))
return ERR_PTR(-EINVAL);
}
// 复制当前的 task_struct,此时父子进程的内容完全一样, 接下来就是重新设置子进程的一些值
p = dup_task_struct(current);
//初始化互斥变量
rt_mutex_init_task(p);
retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;
//检查进程数是否超过 max_threads
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
// 初始化自旋锁
spin_lock_init(&p->alloc_lock);
// 初始化挂起信号
init_sigpending(&p->pending);
// 初始化 CPU 定时器
p->utime = p->stime = p->gtime = 0;
p->utimescaled = p->stimescaled = 0;
//初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
retval = sched_fork(clone_flags, p);
//复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
shm_init_task(p);
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_files(clone_flags, p); //文件描述符
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p); //进程当前工作目录信息
if (retval)
goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p); //信号处理表
if (retval)
goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p); //信号值的处理
if (retval)
goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p); //内存描述符
if (retval)
goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p); //命名空间
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
//初始化子进程内核栈
retval = copy_thread(clone_flags, stack_start, stack_size, p);
//为新进程分配新的pid
if (pid != &init_struct_pid) {
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (!pid)
goto bad_fork_cleanup_io;
}
//设置子进程的pid
p->pid = pid_nr(pid);
//调用fork的进程为其父进程
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;
}
spin_lock(¤t->sighand->siglock);
return p;
}
dup_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);
int err;
tsk = alloc_task_struct_node(node); //申请进程描述符
if (!tsk)
return NULL;
ti = alloc_thread_info_node(tsk, node); // 申请线程描述符
if (!ti)
goto free_tsk;
//将父进程的进程描述符,线程描述符,内核栈的值赋值给子进程
err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_ti;
tsk->stack = ti; //子进程的进程描述符的stack指针设为子进程的线程描述符
return tsk;
free_ti:
free_thread_info(ti);
free_tsk:
free_task_struct(tsk);
return NULL;
}
本次实验是基于实验楼中现有的实验环境进行的。
进入menu文件夹,编辑test.c文件:
cd ~/LinuxKernel/menu/
sudo vim test.c
给qemu增加一个使用fork系统调用的菜单命令,如下所示:
在menu目录下执行如下命令:make rootfs
启动MenuOS,结果如下所示:
使用GDB进行跟踪调试,设置如下断点:
在MenuOS中输入fork菜单命令以后,后面的断点依次如图所示:
首先停在sys_clone位置处:
然后进入do_fork中:
接着进入copy_process中:
接着进入copy_thread中:
最后进入ret_from_fork中:
- Linux内核通过复制父进程来创建一个新进程,调用do_fork为每个新创建的进程动态地分配一个task_struct结构。copy_thread()函数中的代码
p->thread.ip = (unsigned long) ret_from_fork;
将子进程的 ip 设置为 ret_form_fork 的首地址,所以fork系统调用产生的子进程在系统调用处理过程中从ret_from_fork处开始执行。- copy_thread()函数中的代码
*childregs = *current_pt_regs();
将父进程的regs参数赋值到子进程的内核堆栈,里面存放了SAVE ALL中压入栈的参数,之后的RESTORE_ALL宏定义会恢复保存到堆栈中的寄存器的值。- fork系统调用发生一次,但是返回两次。父进程中返回值是子进程的进程号,子进程中返回值为0,可以通过返回值来判断当前进程是父进程还是子进程。
- 整个fork系统调用的执行流程如下:
fork->sys_clone->do_fork->copy_process->dup_task_struct->copy_thread->ret_from_fork。
程序的编译链接过程需要经历如下步骤:
ELF文件的全称是Executable and Linkable Format,意为可执行的、可连接的格式。
ELF文件大致可以分为如下三类:
- 可重定位文件,保存代码和适当的数据,和其他的共享文件一起创建一个可执行文件或者一个共享文件。
- 可执行文件,保存一个可执行的程序,该文件指出exec(BA_OS)如何创建程序进程映像。
- 共享文件,保存代码和合适的数据,被链接编辑器(静态链接)和动态链接器进行链接。
ELF可执行文件格式的具体分析可以参考如下链接。
函数原型 | int execve(const char *path,const char *argv[],const char *envp[]) |
---|---|
头文件 | #include |
参数说明 | path:可执行文件的路径名 argv:命令行参数 envp:环境变量 |
返回值 | 成功则不返回,失败则返回-1 |
编辑如下文件test.c:
#include
void main()
{
char *argv[]={"ls","-al","/etc/passwd",(char *)0 };
char *envp[]={"PATH=/bin",0};
execve("/bin/ls",argv,envp);
}
运行可执行文件test,如下所示:
gcc test.c -o test
./test
在实验楼提供的环境中,给qemu增加一个使用execve系统调用的菜单命令,如下所示:
在menu目录下执行如下命令:make rootfs
启动MenuOS,结果如下所示:
使用GDB进行跟踪调试,设置如下断点:
在MenuOS中输入execve菜单命令以后,截图如下所示:
do_execve函数源代码如下所示:
int do_execve(struct filename *filename,const char __user *const __user *__argv,const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execve_common(filename, argv, envp); // 此处调用do_execve_common
}
- 装载和启动一个可执行程序的大致流程如下所示:
sys_execve -> do_execve-> do_execve_common-> exec_binprm-> search_binary_handler -> load_elf_binary-> start_thread- 对于静态链接的可执行文件,eip指向该文件的文件头e_entry所指的入口地址;对于动态链接的可执行文件,eip指向动态链接器。执行静态链接程序时,execve系统调用修改内核堆栈中保存的eip的值作为新的进程的起点。
- 新的可执行程序修改内核堆栈eip为新程序的起点,从new_ip开始执行,start_thread把返回到用户态的位置从int 0x80的下一条指令变成新的可执行文件的入口地址。
- 执行execve系统调用时,调用execve的可执行程序陷入内核态,使用execve加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的起点(main函数),故新的可执行程序能够顺利执行。
- 中断处理过程(时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
- 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,内核线程作为一类的特殊的进程既可以进行主动调度,也可以进行被动调度;
- 用户态进程无法实现主动调度,只能够通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
在实验楼提供的环境中,设置断点如下所示:
schedule()函数用于实现进程调度,它的任务是从运行队列的链表中找到一个进程,并且随后将CPU分配给这个进程。
从本质上来说,每个进程切换分为两步:
- 切换页全局目录以安装一个新的地址空间;
- 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包括CPU寄存器。
函数和宏的调用关系如下所示:
- schedule() --> context_switch() --> switch_to --> __switch_to()
- 其中__switch_to()函数主要完成硬件上下文切换,switch_to宏主要完成内核堆栈切换。
#define switch_to(prev, next, last) // prev指向当前进程,next指向被调度的进程
do {
unsigned long ebx, ecx, edx, esi, edi;
asm volatile("pushfl\n\t" /* 将标志位压栈 */
"pushl %%ebp\n\t" /* 将当前ebp压栈 */
"movl %%esp,%[prev_sp]\n\t" /* 保存当前进程的堆栈栈顶*/
"movl %[next_sp],%%esp\n\t" /* 将下一个进程的堆栈栈顶保存到esp寄存器,完成内核堆栈的切换*/
"movl $1f,%[prev_ip]\n\t" /* 保存当前进程的eip*/
"pushl %[next_ip]\n\t" /*将下一个进程的eip压栈 */
"jmp __switch_to\n" /*jmp通过后面的寄存器eax、edx来传递参数,__switch_to()函数通过return把next_ip弹出来 */
"1:\t"
"popl %%ebp\n\t" /*恢复当前堆栈的ebp*/
"popfl\n" /* 恢复当前堆栈的寄存器标志位*/
/* output parameters */
: [prev_sp] "=m" (prev->thread.sp), // 当前内核堆栈的栈顶
[prev_ip] "=m" (prev->thread.ip), // 当前进程的eip
"=a" (last),
/* clobbered output registers: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)
/* input parameters: */
: [next_sp] "m" (next->thread.sp), // 下一个进程的内核堆栈的栈顶
[next_ip] "m" (next->thread.ip), // 下一个进程的eip
/* regparm parameters for __switch_to(): */
[prev] "a" (prev), // 寄存器的传递
[next] "d" (next));
__switch_canary_iparam
: /* reloaded segment registers */
"memory");
} while (0)
由上述分析可知,switch_to宏的执行完成了进程切换的第二步,主要完成了内核堆栈的切换。
进程上下文切换与中断上下文切换的关系
- 对于用户态进程:用户态进程->中断上下文切换->进程进入内核态->需要切换进程->内核态调用schedule()函数,完成进程上下文的切换->新进程处于内核态中->中断上下文切换->新进程由内核态返回用户态。
- 对于内核态线程:内核态线程主动调用schedule()函数,只有进程上下文的切换,没有发生中断上下文的切换。