本章的作业依旧包括两部分,1.阅读学习教材「Linux内核设计与实现 (Linux Kernel Development)」第教材第11,12章。
2.学习MOOC「Linux内核分析」第六讲「进程的执行和进程的切换」,并完成实验楼上配套实验六。
在本次试验中,我们首先新建了一个hello.c的文件,并在里面编辑简单的输出hello world的代码。
调用命令gcc -E -o hello.cpp hello.c -m32编辑出预处理的中间文件hello.cpp.预处理负责把include的文件包含进来及宏替换等工作
然后我们调用命令gcc -x cpp-output -S -o hello.s hello.cpp -m32编译hello.cpp为代码目标代码hello.s。ELF格式,二进制文件,有一些机器指令,只是还不能运行
接着继续调用gcc -x assmebler -c hello.s -o hello.o -m32汇编成他的二进制文件。ELF格式,二进制文件,有一些机器指令,只是还不能运行
接下来调用gcc -o hello hello.o -m32链接成它的可执行文件并运行。(ELF格式,二进制文件)
这里的可执行文件已经链接成功了,我们打开hello查看数据,发现开头有elf,这里的elf就是指的它的文件格式。在hello可执行文件里面使用了共享库,会调用printf,libc库里的函数
这里的printf使用的是共享库,我们试着使用静态编译。gcc -o hello.static hello.o -m32 -static 把执行所需要依赖的东西都放在程序内部
接下来我们了解了具体命令的含义。
-c
只激活预处理,编译,和汇编,也就是他只把程序做成obj文件
例子用法:
gcc -c hello.c
他将生成.o的obj文件
-S
只激活预处理和编译,就是指把文件编译成为汇编代码。
例子用法
gcc -S hello.c
他将生成.s的汇编代码,你可以用文本编辑器察看
-E
只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里
面.
例子用法:
gcc -E hello.c > pianoapan.txt
gcc -E hello.c | more
慢慢看吧,一个hello word 也要与处理成800行的代码
-o
制定目标名称,缺省的时候,gcc 编译出来的文件是a.out,很难听,如果
你和我有同感,改掉它,哈哈
例子用法
gcc -o hello.exe hello.c (哦,windows用习惯了)
gcc -o hello.asm -S hello.c
接下来使用ls -l查看每一个文件的属性。
从这里可以看出动态链接的可执行文件hello的内存远远小于静态链接的hello.static。
为了更清楚的了解程序执行的过程,我们调用readelf -h hello来查看hello的文件。
这里面包含程序运行的所有信息,包括版本号,兼容格式,ABI的版本,是不是可执行文件,入口地址,数据段的原数据。
下面使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve。
在他里面我们对三个函数打上断点进行观察,分别是sys_execve,load_elf_binary,start_thread.
通过gdb调试分析可以知道对于execve系统调用的执行流程是
sys_execve
do_execve
do_execve_common
exec_binprm ->
search_binary_handler
load_binary
load_elf_binary (也执行了elf_format)
start_thread。
当系统调用execve时,系统陷入内核,这时会创建一个新的用户态堆栈,把命令行参数的内容和环境变量的内容通过指针的方式传递给系统调用内核处理函数的,然后内核处理函数在创建可执行程序新的用户态堆栈的时候,会把这些拷贝到用户态堆栈初始化新的可执行程序的执行上下文环境。这时就加载了新的可执行程序。系统调用exceve返回用户态的时候,就变成了被exceve加载的可执行程序。
首先sys_execve调用了do_execve,该函数将参数和环境变量的数据结构进行修改后调用了do_execve_common。
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);//其中,filenamei是可执行文件路径名的地址,argv是命令行参数指针数组(最后一个元素为NULL)的地址,envp是环境变量指针数组(最后一个元素也为NULL)的地址。
}
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);//其中,filenamei是可执行文件路径名的地址,argv是命令行参数指针数组(最后一个元素为NULL)的地址,envp是环境变量指针数组(最后一个元素也为NULL)的地址。
}
继续看,do_execve_common函数可以抽象为下面的结构:
int do_execve_common()
{
file = do_open_exec(filename); //打开要加载的可执行文件
...
各种初始化bprm//初始化linux_binprm结构体变量*bprm的file、filename和interp三个字段。
...
exec_binprm(bprm); //加载程序
}
其中加载程序的exec_binprm函数中,调用了关键的search_binary_handler(bprm),该函数遍历链表来尝试加载目标文件,找到了则执行load_binary.
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
put_binfmt(fmt);
bprm->recursion_depth--;
...
目标文件的格式是ELF,所以相应的load_binary为load_elf_binary,该函数可以抽象如下:
load_elf_binary()
{
...
解析ELF文件
...
elf_map(bprm->file, load_bias + vaddr,...) //把目标文件映射到地址空间中
...
if (elf_interpreter) {把elf_entry设置为动态链接器ld的起点}
else {目标文件的入口赋值给elf_entry}
...
start_thread(..., elf_entry, ...);
}
该函数的核心工作一是解析ELF文件,二是把目标文件映射到进程空间中,三是调用start_thread。start_thread实际上在修改了内核堆栈后调用iret返回用户态,把我们返回用户态的位置从int 0x80的下一条指令的位置变成新加载的可执行文件的entry位置(new_ip)。
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs = 0;
regs->ds = __USER_DS;
regs->es = __USER_DS;
regs->ss = __USER_DS;
regs->cs = __USER_CS;
regs->ip = new_ip;
regs->sp = new_sp;
regs->flags = X86_EFLAGS_IF;
/*
* force it to the iret return path by making it look as if there was
* some work pending.
*/
set_thread_flag(TIF_NOTIFY_RESUME);
}
Linux任务切换是通过switch_to实现的。switch_to本身是一个宏,通过利用长跳指令,当长跳指令的操作数是TSS描述符的时候,就会引起CPU的任务的切换,此时,cpu将所有寄存器的状态保存到当前任务寄存器TR所指向的TSS段(当前任务的任务状态段)中,然后利用长跳指令的操作数(TSS描述符)找到新任务的TSS段,然后将其中的内容填写到各个寄存器中,最后,将新任务的TSS选择符更新到TR中。这样系统就正式开始运行新切换的任务了。
在MenOS中对schedule、context_switch、switch_to设置断点,
进程切换调用的是schedule()函数,该函数调用了__schedule().
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
__schedule();
}
static void __sched __schedule(void)
{
...
next = pick_next_task(rq, prev);
if (likely(prev != next)) {
...
context_switch(rq, prev, next); /* unlocks the rq */
...
} else {
...
raw_spin_unlock_irq(&rq->lock);
...
}
...
post_schedule(rq);
...
}
对于其中的context_switch()
context_switch {
...
mm = next->mm;
if(!mm) {
next->active_mm = oldmm;
...
}
else
switch_mm(oldmm, mm, next);
...
switch_to(prev, next, prev);
...
}
该函数做了两件事情,第一是切换页表switch_mm,当next进程mm为空(next是内核线程)时,则使用当前进程的页表,否则切换成新进程的页表(用户态地址空间);第二是切换进程switch_to
#define switch_to(prev, next, last) \
do {
unsigned long ebx, ecx, edx, esi, edi; \
asm volatile("pushfl\n\t" /*保存当前进程的flag */ \
"pushl %%ebp\n\t" /* 保存当前进程EBP */ \
"movl %%esp,%[prev_sp]\n\t" /* 保存当前的内核栈顶 */ \
"movl %[next_sp],%%esp\n\t" /* 恢复下一个进程的内核栈顶 */ \
//内核堆栈角度,这里已经切换到next的内核堆栈了
"movl $1f,%[prev_ip]\n\t" /*将标号1放入当前进程的EIP*/ \
"pushl %[next_ip]\n\t" /* 恢复下一个进程的EIP,next内核堆栈的栈顶 */ \ \
"jmp __switch_to\n" /*寄存器传递参数,jmp不压栈EIP*/ \
//EIP角度,这里是新的进程的执行入口
"1:\t" /*switch_to 返回到这里*/ \
"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)
1.更新系统运行时间。
2.更新实际时间。
3.在smp系统上,均衡调度程序中各处理器上的运行队列。
4.检查当前进程是否用尽了自己的时间片。
5.运行超时的动态定时器。
6.更新资源消耗和处理器时间的统计值。
1.更高的中断解析度可提高时间驱动时间的解析度。
2.提高了时间驱动的准确度。
1.内核定时器能够已更高的频度和更高的准确度运行。
2.依赖定时值执行的系统调用能够以更高的精度运行。
3.对诸如资源消耗和系统运行时间等的测量会有更精细的解析度。
4.提高进程抢占的准确度。
1.获得xtime——lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。
2.需要时应答或重新设置系统时钟。
3.周期性地使用墙上时间更新实时时钟。
4.调用体系结构无关的时钟例程:tick_periodic();
struct timer_list {
struct list_head entry;//定时器链表入口
unsigned long expires;//以jiffies为单位的定时值
void (*function) (unsigned long);//定时器处理函数
unsigned long data;//传给处理函数的长整型参数
struct tvec_t_base_s *base;//定时器内部值,用户不要使用
}
add_timer(&my_timer);//激活定时器
mod_timer(&my_timer,jiffies+new_delay);//修改超时时间
del_timer(&my_timer);//停止定时器
struct page {
unsigned long flags;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架