原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
作者:136
内核信息:linux-3.18.6
——下载
进程是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称。Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,即进程描述符或进程控制块(PCB),这个结构体包含了一个进程所需的所有信息。它定义在linux-3.18.6/include/linux/sched.h
文件中。
1. 进程状态
volatile long state;
定义了一个 long 型的进程的状态state
,取值范围如下所示。
#define TASK_RUNNING 0 //进程要么正在执行,要么正要准备执行
#define TASK_INTERRUPTIBLE 1 //进程被阻塞(睡眠),直到某个条件变为真
#define TASK_UNINTERRUPTIBLE 2 //与TASK_INTERRUPTIBLE类似,但不能通过接受一个信号来唤醒以外
#define __TASK_STOPPED 4 //进程被停止执行
#define __TASK_TRACED 8 //进程被debugger等进程监视
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16 //进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息(僵尸模式)
#define EXIT_DEAD 32 //进程的最终状态
/* in tsk->state again */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
2. 进程标识符
pid_t pid;
pid_t tgid; #一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID
在CONFIG_BASE_SMALL配置为0的情况下,PID的取值范围是0到32767,即系统中的进程数最大为32768个。
3. 进程内核栈
void *stack;
进程通过alloc_thread_info函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。
static inline struct thread_info *alloc_thread_info(struct task_struct *tsk)
{
#ifdef CONFIG_DEBUG_STACK_USAGE
gfp_t mask = GFP_KERNEL | __GFP_ZERO;
#else
gfp_t mask = GFP_KERNEL;
#endif
return (struct thread_info *)__get_free_pages(mask, THREAD_SIZE_ORDER);
}
static inline void free_thread_info(struct thread_info *ti)
{
free_pages((unsigned long)ti, THREAD_SIZE_ORDER);
}
4. 其他参数
//标记
unsigned int flags; /* per process flags, defined below */
//进程亲属关系的成员
struct task_struct *real_parent; /* real parent process */
struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
//进程调度
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
unsigned int policy;
cpumask_t cpus_allowed;
//进程地址空间
struct mm_struct *mm, *active_mm;
#ifdef CONFIG_COMPAT_BRK
unsigned brk_randomized:1;
#endif
#if defined(SPLIT_RSS_COUNTING)
struct task_rss_stat rss_stat;
#endif
//信号处理
/* signal handlers */
struct signal_struct *signal; //指向进程的信号描述符
struct sighand_struct *sighand; //指向进程的信号处理程序描述符
sigset_t blocked, real_blocked; //blocked表示被阻塞信号的掩码,real_blocked表示临时掩码
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending; //存放私有挂起信号的数据结构
unsigned long sas_ss_sp; //信号处理程序备用堆栈的地址
size_t sas_ss_size; //表示堆栈的大小
int (*notifier)(void *priv); //指向的函数来阻塞进程的某些信号
void *notifier_data; //notifier所指向的函数可能使用的数据
sigset_t *notifier_mask; //位掩码
还有其他成员的代码分析,可以参阅博客:https://www.cnblogs.com/zxc2man/p/6649771.html
。
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;
int trace = 0;
long nr;
// ...
//copy PCB and return a created task_struct point
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);
//get pid of task struct
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
//if use vfork
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
//add child process to queue and ready to get CPU
wake_up_new_task(p);
// ...
//if set a CLONE_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;
}
do_fork
函数调用copy_process
,复制父进程task_struct
来创建一个新进程,要给新进程分配一个新的内核堆栈;然后调用wake_up_new_task
将子进程加入调度器,为之分配 CPU,如果是VFORK
,则父进程等待子进程完成 exec 替换自己的地址空间。
1. copy_process()
流程
dup_task_struct()
复制当前的 task_struct,检查进程数是否超过限制;sched_fork
初始化进程数据结构,并把进程状态设置为 TASK_RUNNING,复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等;copy_thread()
初始化子进程内核栈,为新进程分配并设置新的pid。2. dup_task_struct()
流程
alloc_task_struct_node()
分配一个 task_struct 节点;alloc_thread_info_node()
分配一个 thread_info 节点,其实是分配了一个thread_union联合体,将栈底返回给 ti;3. copy_thread
的流程
thread.sp
赋值,将来子进程运行,这就是子进程的esp寄存器的值。ret_from_kernel_thread
,将这段代码的地址赋给thread.ip
,之后准备其他寄存器信息,退出ret_from_fork
开始执行,所以它的地址赋给thread.ip
,也就是将来的eip寄存器。4. 新进程从ret_from_fork
处开始执行
dup_task_struct
中为其分配了新的堆栈copy_process
中调用了sched_fork
,将其置为TASK_RUNNINGcopy_thread
中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。ret_from_fork
的地址设置为eip寄存器的值,这是子进程的第一条指令。总的来说fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;具体过程如下:
fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_fork()
在上一次的基础上添加一个Fork()
函数,来调用do_fork
内核处理函数,函数代码如下。
int Fork(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid<0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid==0)
{
/* child process */
printf("This is Child Process, and my pid is %d\n", pid);
}
else
{
/* parent process */
printf("This is Parent Process, and my pid is %d\n", pid);
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
将程序保存为test_fork.c
文件,并修改 Makefile 文件,重新编译 MenuOS。
$ make rootfs #重新编译
输入“fork”,执行我们的Fork()
函数,可以看到输出了相应的信息。
确认程序无误之后,就可以进行调试了。
$ qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr
$ gdb vmlinux #启动gdb调试
(gdb)b sys_clone #设置断点
(gdb)b _do_fork
(gdb)b dup_task_struct
(gdb)b copy_process
(gdb)b copy_thread
b ret_from_for
(gdb)target remote:1234 #设置远程链接
(gdb)c #continue,跳到端点处
(gdb)s #step,一步步调试
程序首先停止在系统调用__se_sys_clone
函数,一步步调试,分别进入 _do_fork、dup_task_struct、copy_process、copy_thread、ret_from_for 断点处。
建议使用内核Linux-3.18.6
来进行实验,这里用到的 5.0.1 内核有些函数名可能发生了变化。
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。
第一步:先编辑一个test.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("test for linux...");
return 0;
}
第二步:生成预处理文件hello.cpp(预处理负责把include的文件包含进来及宏替换等工作)
$ gcc -E -o test.cpp test.c -m32
第三步:编译成汇编代码hello.s
$ gcc -x cpp-output -S -o test.s test.cpp -m32
第四步:编译成目标代码,得到二进制文件hello.o
$ gcc -x assembler -c test.s -o test.o -m32
第五步:链接成可执行文件hello,(它是二进制文件)
$ gcc -o test test.o -m32
第六步:运行一下./hello
$ ./test
我们也可以静态编译,(是完全把所有需要执行所依赖的东西放到程序内部)
$ gcc -o test.static test.o -m32 -static
hello.static 也是ELF格式文件,运行一下hello.static ./hello.static。发现hello.static 比 hello 占的空间大的多。
$ b do_execve
由跟踪结果可知,当调用新的可执行程序时,会先进入内核态调用do_execve处理函数,并使用堆栈对原来的现场进行保护。然后,根据返回的可执行文件的地址,对当前可执行文件进行覆盖。由于返回地址为调用可执行文件的main函数入口,所以可以继续执行该文件。
$ b schedule
$ b pick_next_task
$ b context_switch
$ b __switch_to
由以上跟踪结果可以得知,在进行进程间的切换时,各处理函数的调用顺序如下:pick_next_task -> context_switch -> __switch_to 。由此可以得出,当进程间切换时,首先需要调用pick_next_task函数挑选出下一个将要被执行的程序;然后再进行进程上下文的切换,此环节涉及到“保护现场”及“现场恢复”;在执行完以上两个步骤后,调用__switch_to进行进程间的切换。
schedule() --> context_switch() --> switch_to --> __switch_to()
asm volatile("pushfl\n\t" /* 保存当前进程的标志位 */
"pushl %%ebp\n\t" /* 保存当前进程的堆栈基址EBP */
"movl %%esp,%[prev_sp]\n\t" /* 保存当前栈顶ESP */
"movl %[next_sp],%%esp\n\t" /* 把下一个进程的栈顶放到esp寄存器中,完成了内核堆栈的切换,从此往下压栈都是在next进程的内核堆栈中。 */
"movl $1f,%[prev_ip]\n\t" /* 保存当前进程的EIP */
"pushl %[next_ip]\n\t" /* 把下一个进程的起点EIP压入堆栈 */
__switch_canary
"jmp __switch_to\n" /* 因为是函数所以是jmp,通过寄存器传递参数,寄存器是prev-a,next-d,当函数执行结束ret时因为没有压栈当前eip,所以需要使用之前压栈的eip,就是pop出next_ip。 */
"1:\t" /* 认为next进程开始执行。 */
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
/* output parameters 因为处于中断上下文,在内核中
prev_sp是内核堆栈栈顶
prev_ip是当前进程的eip */
: [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip), //[prev_ip]是标号
"=a" (last),
/* clobbered output registers: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)
__switch_canary_oparam
/* input parameters:
next_sp下一个进程的内核堆栈的栈顶
next_ip下一个进程执行的起点,一般是$1f,对于新创建的子进程是ret_from_fork*/
: [next_sp] "m" (next->thread.sp),
[next_ip] "m" (next->thread.ip),
/* regparm parameters for __switch_to(): */
[prev] "a" (prev),
[next] "d" (next)
__switch_canary_iparam
: /* reloaded segment registers */
"memory");
} while (0)
switch_to
实现了进程之间的真正切换:
__switch_to()
Linux 系统中的fork
系统调用。fork
会创建一个新的进程,加载文件并进行执行。在这个过程中,涉及到了两个进程之间的切换。我们依然使用前一篇文章的环境,对fork
系统调用进行调试,来完成这个分析。当我们调用fork
函数的时候,产生了软中断,通过int 0x80陷入内核,进入sys_call
函数,并且通过eax传递系统调用号参数。然后,通过sys_call_table
来寻找相应的系统调用函数进行执行。在这里是sys_clone
。这个是我们的第一个断点。
对于进程的切换,主要有两部分需要理解,一个是进程切换的时机,一个是schedule
函数的调用过程。对于进程切换的时机,中断处理以后是其中一个时机,内核线程也可以进程schedule
函数的调用,来完成进程的切换。对于schedule
函数。大致的过程是: 首先,进程x在执行,假设执行的ip是 ip_prev.
进入中断处理,以及进程切换的时机以后,先保持自己的内核栈信息,包括 flags 以及 ebp,自己的 ip 会保存在 thread 结构体中。这里的 ip 设置成了标号1的位置。也就是,如果进程切换了,下次回到这个进程的时候,会执行标号1的位置开始执行,回复 flags 以及 ebp。所以这里的保持flags/ebp 和恢复是对应。对于新的进程开始执行的位置,如果是像fork
这样的创建的新进程,从 thread.ip 中取出来的就是ret_from_fork
,如果是之前运行过的进程,就如上面说的,进入标号1的位置开始执行。这里的的保持 ip 和恢复 ip 也是配套的。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有