学号084,原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
一、关于struct task_struct
Linux内核内部通过struct task_struct来表示进程描述符用于管理进程,其功能类似操作系统中我们常常提到进程控制块。由于struct task_struct内容涉及过于宽泛,其设计本身庞大而繁杂,这里只对关键字段进行一定分析。
1.进程状态
volatile long state;
int exit_state;
state可能值如下:
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16
#define EXIT_DEAD 32
/* in tsk->state again */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
这里主要介绍集中较为熟悉的状态:
TASK_RUNING:进程处于执行或将要执行的状态
TASK_INTERRUPTIBLE:进程因为等待一些条件被挂起的状态
TASK_STOPPED:表示进程被停止执行的状态
2.进程标识
pid_t pid;
pid_t tgid;
一般而言,对于应用往往可以通过调用getpid()来获取进程ID。对于内核而言,可以通过调用current宏来获取其进程标识符。
3.进程列表 task_struct中有一list_head类型的字段,其相当于双向链表的一个节点。通过这个节点,每一个task_struct结构被链接在进程列表中。
4.进程分配与管理 内核通过slab allocator分配task_struct,在栈的底部存放一个新的结构体struct thread_info,这个结构体中存在一个指向slab allocator分配的struct task_struct结构体的指针。
5.进程间关系
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 *
在Linux内核中,进程被组织成一棵树的形式,所有的后代都是init的后代。其中:
real_parent指向父进程,如果父进程不存在,则指向pid为1的init进程;
parent指向父进程,当终止时,必须要想父进程发出信号;
children表示链表头部,链表中的元素都是它的子进程;
sibling用于把当前进程插入到兄弟链表中;
group_leader指向其所在进程组的领头进程。
二、关于fork()函数的相关分析
1.基础知识
fork()函数主要用于产生进程,使得多道批处理程序可以执行。fork()一次调用后会返回两次结果,并且为三种不同的返回值:
a.在父进程中,fork返回新创建子进程的进程ID
b.在子进程中,fork返回0
c.如果出现错误,fork返回结果为一个负值
2.实验过程
首先我们定义了一个含有fork()调用的函数:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
int pid;
pid = fork();
if (pid < 0){
printf("Error Forking!");
}
else if (pid == 0){
printf("Child Process!");
}
else{
printf("Parent Process!");
}
}
然后编译并执行内核代码:
ok! 运行成功。现在我们进入gdb,并设置断点:
现在我们来看看输出过程吧。
3.结果分析
首先fork函数通过0x80中断陷入内核。而后我们追踪到了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;
// ...
// 复制进程描述符,返回创建的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);
// 取出task结构体内的pid
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
// 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
// 将子进程添加到调度器的队列,使得子进程有机会获得CPU
wake_up_new_task(p);
// ...
// 如果设置了 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()执行过程:
a.调用copy_process为子进程复制出一份进程信息,若为vfork则完成初始化信息处理。
b.调用wake_up_new_task将紫禁城加入调度器,为之分配CPU,若为vfork,父进程等待子进程完成exec替换自己的地址空间。
do_fork()执行完成后,执行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 = dup_task_struct(current);
//……
//初始化互斥变量
rt_mutex_init_task(p);
//检查进程数是否超过限制,由操作系统定义
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
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 定时器
posix_cpu_timers_init(p);
//……
//初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
retval = sched_fork(clone_flags, p);
//复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
if (retval)
goto bad_fork_cleanup_policy;
retval = perf_event_init_task(p);
if (retval)
goto bad_fork_cleanup_policy;
retval = audit_alloc(p);
if (retval)
goto bad_fork_cleanup_perf;
/* copy all the process information */
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);
//初始化子进程内核栈
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);
//……
//返回结构体 p
return p;
}
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;
//分配一个 task_struct 节点
tsk = alloc_task_struct_node(node);
if (!tsk)
return NULL;
//分配一个 thread_info 节点,包含进程的内核栈,ti 为栈底
ti = alloc_thread_info_node(tsk, node);
if (!ti)
goto free_tsk;
//将栈底的值赋给新节点的栈
tsk->stack = ti;
//……
return tsk;
}
int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{
//获取寄存器信息
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;
p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
if (unlikely(p->flags & PF_KTHREAD)) {
//内核线程
memset(childregs, 0, sizeof(struct pt_regs));
p->thread.ip = (unsigned long) ret_from_kernel_thread;
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl();
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr = NULL;
return 0;
}
//将当前寄存器信息复制给子进程
*childregs = *current_pt_regs();
//子进程 eax 置 0,因此fork 在子进程返回0
childregs->ax = 0;
if (sp)
childregs->sp = sp;
//子进程ip 设置为ret_from_fork,因此子进程从ret_from_fork开始执行
p->thread.ip = (unsigned long) ret_from_fork;
//……
return err;
}
copy_process()执行过程:
调用dup_task_struct复制到当前的task_struct,包括调用alloc_task_struct_node分配一个task_struct结点,调用alloc_thread_info_node分配一个thread_info结点;
检查进程数是否超过限制;
初始化自旋锁、挂起信号、CPU定时器等;
调用sched_fork初始化进程数据结构,并把进程状态设置为TASK_RUNNING;
复制所有进程信息;
调用copy_thread初始化子进程内核栈;
为新进程分配并设置新的pid;
这样,基本完成了子进程的创建,因为p->thread.ip = (unsigned long) ret_from_fork;
子进程最终通过ret_from_fork执行。又因为copy_thread中将父进程的寄存器上下文复制给子进程,从而保证了父子进程的栈堆信息是一致的。
三、编译链接的过程与ELF可执行文件
1.链接编译的过程
编译是读取源程序,对之进行词法分析语法分析,将高级语言指令转换成功能等效的汇编代码,其主要包含两个阶段:
(1)预处理阶段,根据已配置文件中的预处理指令来修改源文件内容。 (2)编译阶段,通过词法分析语法分析,确认所有指令都符合语法规则,将其翻译成等价的中间代码或汇编代码。
汇编实际上是将汇编语言的相关代码翻译成目标机器指令的过程。 链接过程是由汇编程序生成的目标文件并不能立即被执行,需要通过链接过程将相关的文件批次链接,使得目标文件能够成为一个能够按操作系统装入执行的统一整体。
2.ELF可执行文件
ELF文件原名Executable and Linking Format,即可执行可连接格式。
ELF文件有以下三种类型:
- 可重定位文件(Relocatable File) 包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。 (Linux的*.o 文件 Windows的 *.obj文件)
- 可执行文件(Executable File) 包含适合于执行的一个程序,此文件规定了 exec() 如何创建一个程序的进程映像。(比如/bin/bash文件;Windows的*.exe)
- 共享目标文件(Shared Object File) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。
ELF文件作用主要有两个:一是用于构建程序,构建动态链接库或可执行文件,主要体现在链接过程。二是用于运行程序。
目标文件的组成部分:
- ELF头文件:位于文件最开始处,包含整个文件的结构信息。
- 节:包含指令数据,符号数据,重定位数据等。
- 程序头表:主要负责在运行过程中,在链接过程中亦可选择,负责告诉系统如何创建进程的映像。
- 节头表:包含文件中所用节的信息。
ELF头文件在Linux中的定义:
/* 32-bit ELF base types. */
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
#define EI_NIDENT 16
typedef struct elf32_hdr
{
unsigned char e_ident[EI_NIDENT]; //16字节的信息,下文详细解释
Elf32_Half e_type; //目标文件类型
Elf32_Half e_machine; //体系结构类型
Elf32_Word e_version; //目标文件版本
Elf32_Addr e_entry; /* Entry point 程序入口的虚拟地址*/
Elf32_Off e_phoff; //程序头部表的偏移量
Elf32_Off e_shoff; //节区头部表的偏移量
Elf32_Word e_flags; //
Elf32_Half e_ehsize; //ELF头部的大小
Elf32_Half e_phentsize; //程序头部表的表项大小
Elf32_Half e_phnum; //程序头部表的数目
Elf32_Half e_shentsize; //节区头部表的表项大小
Elf32_Half e_shnum; //节区头部表的数目
Elf32_Half e_shstrndx; //
} Elf32_Ehdr; //此结构体一共52个字节
四、静态链接与动态链接实验
目标文件:
hello1.c
main.c
1.静态链接
gcc -c hello1.c //生成hello1.o可执行文件
ar rs libhello.a hello1.o //生成libhello.a静态链接库
sudo cp libhello.a /usr/lib //将静态链接库拷贝至共享链接库目录下
gcc -o hello main.c -lhello //利用静态链接库生成可执行文件
./hello
2.动态链接
gcc -fpic -c hello1.c //-c意为只编译不链接,-fpic意为位置独立代码, 指示编译程序生成的代码要适合共享库的内容这样的代码能够根据载入内存的位置计算内部地址
gcc hello1.o -shared -o libhello.so //生成动态链接库
sudo cp libhello.so /usr/lib //复制文件成为共享动态链接库
gcc -o hello main.c libhello.so //动态链接文件编译
./hello
3.总结
静态链接库的一个缺点是,如果我们同时运行了很多程序,并且使用了同一个库函数,这样在内存中就会大量拷贝同一个库函数,这样会浪费很多内存和存储空间。
当一个程序使用动态库函数时,在链接阶段并不把函数代码链接进来,而只是链接函数的一个引用,当最终的函数倒入内存开始真正运行时,函数引用被解析,共享函数库的代码才真正被倒入到内存中,这样共享链接库的函数就可以被许多程序同时共享,而且只存储一次即可。同时动态库可以独立更新,与调用它的程序互不影响。
五、exec系统函数分析
1.基础知识
exec系列头文件
int execl(char const *path, char const *arg0, ...);
int execle(char const *path, char const *arg0, ..., char const *envp[]);
int execlp(char const *file, char const *arg0, ...);
int execv(char const *path, char const *argv[]);
int execve(char const *path, char const *argv[], char const *envp[]);
int execvp(char const *file, char const *argv[]);
exec目标程序:
来看看执行结果:
2.实验分析
断点设置:
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
char *pathbuf = NULL;
struct linux_binprm *bprm; /* 这个结构当然是非常重要的,下文,列出了这个结构体以便查询各个成员变量的意义 */
struct file *file;
struct files_struct *displaced;
int retval;
if (IS_ERR(filename))
return PTR_ERR(filename);
/*
* We move the actual failure in case of RLIMIT_NPROC excess from
* set*uid() to execve() because too many poorly written programs
* don't check setuid() return code. Here we additionally recheck
* whether NPROC limit is still exceeded.
*/
if ((current->flags & PF_NPROC_EXCEEDED) &&
atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) {
retval = -EAGAIN;
goto out_ret;
}
/* We're below the limit (still or again), so we don't want to make
* further execve() calls fail. */
current->flags &= ~PF_NPROC_EXCEEDED;
// 1. 调用unshare_files()为进程复制一份文件表;
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
retval = -ENOMEM;
// 2、调用kzalloc()在堆上分配一份structlinux_binprm结构体;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free;
check_unsafe_exec(bprm);
current->in_execve = 1;
// 3、调用open_exec()查找并打开二进制文件;
file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
// 4、调用sched_exec()找到最小负载的CPU,用来执行该二进制文件;
sched_exec();
// 5、根据获取的信息,填充structlinux_binprm结构体中的file、filename、interp成员;
bprm->file = file;
if (fd == AT_FDCWD || filename->name[0] == '/') {
bprm->filename = filename->name;
} else {
if (filename->name[0] == '\0')
pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
else
pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
fd, filename->name);
if (!pathbuf) {
retval = -ENOMEM;
goto out_unmark;
}
/*
* Record that a name derived from an O_CLOEXEC fd will be
* inaccessible after exec. Relies on having exclusive access to
* current->files (due to unshare_files above).
*/
if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
bprm->filename = pathbuf;
}
bprm->interp = bprm->filename;
// 6、调用bprm_mm_init()创建进程的内存地址空间,并调用init_new_context()检查当前进程是否使用自定义的局部描述符表;如果是,那么分配和准备一个新的LDT;
retval = bprm_mm_init(bprm);
if (retval)
goto out_unmark;
// 7、填充structlinux_binprm结构体中的命令行参数argv,环境变量envp
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc) < 0)
goto out;
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc) < 0)
goto out;
// 8、调用prepare_binprm()检查该二进制文件的可执行权限;最后,kernel_read()读取二进制文件的头128字节(这些字节用于识别二进制文件的格式及其他信息,后续会使用到);
retval = prepare_binprm(bprm);
if (retval < 0)
goto out;
// 9、调用copy_strings_kernel()从内核空间获取二进制文件的路径名称;
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0)
goto out;
bprm->exec = bprm->p;
// 10.1、调用copy_string()从用户空间拷贝环境变量
retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
goto out;
// 10.2、调用copy_string()从用户空间拷贝命令行参数;
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out;
/*
至此,二进制文件已经被打开,struct linux_binprm结构体中也记录了重要信息;
下面需要识别该二进制文件的格式并最终运行该文件
*/
retval = exec_binprm(bprm);
if (retval < 0)
goto out;
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
acct_update_integrals(current);
task_numa_free(current);
free_bprm(bprm);
kfree(pathbuf);
putname(filename);
if (displaced)
put_files_struct(displaced);
return retval;
out:
if (bprm->mm) {
acct_arg_size(bprm, 0);
mmput(bprm->mm);
}
out_unmark:
current->fs->in_exec = 0;
current->in_execve = 0;
out_free:
free_bprm(bprm);
kfree(pathbuf);
out_files:
if (displaced)
reset_files_struct(displaced);
out_ret:
putname(filename);
return retval;
}
!
从上述图片中,可以看到主要的处理过程都在do_execve_common()函数。load_elf_binary()函数中,加载进来的可执行文件将把当前正在执行的进程的内存空间完全覆盖掉,如果可执行文件是静态链接的文件,进程的IP寄存器值将被设置为main函数的入口地址,从而开始新的进程;而如果可执行文件是动态链接的,IP的值将被设置为加载器ld的入口地址,是程序的运行由该加载器接管,ld会处理一些依赖的动态链接库相关的处理工作,使程序继续往下执行,而不管哪种执行方式,当前的进程都会被新加载进来的程序完全替换掉。
简单总结以下execve系统调用的执行过程:
1. 陷入内核
2. 加载新的可执行文件并进行可执行性检查
3. 将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据
4. 将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址
5. 返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。execve函数从来不会成功返回。
六、Linux进程切换
1.Linux进程调度时机:
1、进程状态转换的时刻:进程终止、进程睡眠;这种情况下,进程要调用sleep()或exit()等函数进行状态转换,这些函数会主动调用调度程序进行进程调度;
2、当前进程的时间片用完时(current->counter=0),这种情况下,进程的时间片是由时钟中断来更新的;
3、设备驱动程序,当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。
4、进程从中断、异常及系统调用返回到用户态时;不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调用调度程序。从系统调用返回意味着要离开内核态而返回到用户态,而状态的转换要花费一定的时间,因此,在返回到用户态前,系统把在内核态该处理的事全部做完。
2.实验过程
此处目标代码与exec系列函数调用分析的代码基本一致,这里不再赘述。
断点设置:
实验结果展示:
3.实验结果分析 __schedule():
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
...
//调度算法
next = pick_next_task(rq, prev);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
rq->skip_clock_update = 0;
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++*switch_count;
//进程上下文切换
context_switch(rq, prev, next);
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
sched_preempt_enable_no_resched();
if (need_resched())
goto need_resched;
}
/*
其主要功能包含了以下几点:
1.针对抢占的处理
2.自旋锁
3.检查prev的状态,并且重设state的状态
4.进程调度算法(next = pick_next_task(rq, prev);)
5.更新就绪队列的时钟
6.进程上下文切换(context_switch(rq, prev, next);)
*/
context_switch():
该函数主要完成了以下几点:
1.判断是否为内核线程,即是否需要上下文切换(mm)
如果next是一个普通进程,schedule( )函数用next的地址空间替换prev的地址空间
如果prev是内核线程或正在退出的进程,context_switch()函数就把指向prev内存描述符的指针保存到运行队列的prev_mm字段中,然后重新设置prev->active_mm
2.切换堆栈和寄存器(switch_to(prev, next, prev);)宏switch_to用来进行关键上下文切换。
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
arch_start_context_switch(prev);
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
context_tracking_task_switch(prev, next);
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();
finish_task_switch(this_rq(), prev);
}
switch_to实现的结果:
首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
然后将prev的内核堆栈指针ebp存入prev->thread.esp中。
把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中
将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度
通过jmp指令(而不是call指令)转入一个函数__switch_to()恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。
#define switch_to(prev, next, last)
do {
unsigned long ebx, ecx, edx, esi, edi;
asm volatile("pushfl\n\t" /* save flags */
"pushl %%ebp\n\t" /* save EBP */
"movl %%esp,%[prev_sp]\n\t" /* save ESP */
"movl %[next_sp],%%esp\n\t" /* restore ESP */
"movl $1f,%[prev_ip]\n\t" /* save EIP */
"pushl %[next_ip]\n\t" /* restore EIP */
__switch_canary
"jmp __switch_to\n" /* regparm call */
"1:\t"
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
/* output parameters */
: [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip),
"=a" (last),
/* clobbered output registers: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)
__switch_canary_oparam
/* input parameters: */
: [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)
finish_task_switch():
该函数主要包含以下几点:
如果prev是一个内核线程,运行队列的prev_mm 字段存放借给prev的内存描述符的地址。mmdrop( )减少内存描述符的使用计数器,如果该计数器等于0了,函数还要释放与页表相关的所有描述符和虚拟存储区。
finish_task_switch( )函数还要释放运行队列的自旋锁并打开本地中断。然后,检查prev 是否是一个正在从系统中被删除的僵死任务, 如果是,就调用put_task_struct( )以释放进程描述符引用计数器,并撤消所有其余对该进程的引用。
mm = this_rq( )->prev_mm;
this_rq( )->prev_mm = NULL;
prev_task_flags = prev->flags;
spin_unlock_irq(&this_rq( )->lock);
if (mm)
mmdrop(mm);
if (prev_task_flags & PF_DEAD)
put_task_struct(prev);
4.实验总结:
Linux的调度程序是一个叫schedule()的函数,内核线程主动调用schedule(),只有进程上下文的切换
宏switch_to实现了进程之间的真正切换。