引言
本专题是手动实现一个“线程”, 写这个项目的起因主要是为了为学习操作系统的人而准备的练手和巩固线程和进程调用的知识. 而这个小项目是从我所写的玩具操作系统中抽离出来再加以改善的, 所以并不乎涉及更加底层的东西, 毕竟我也不会.
前言
在讲解小项目之前要先明白什么是函数调用? 函数又是怎么实现调用并返回到调用函数的函数体中?
可能你不明白为什么线程会跟函数调用有关, 那是因为我们实现的功能只是并行的, 说白就是函数调用, 并不是并发.
小概念
并行 : 宏观上是并发, 微观是串行. 比如3(1, 2, 3行, 每行表示一个进程)个进程, 但只有一个CPU, 3个进程很快的进行来回切换, 这样我们感觉就是每个进程都在同时的运行(这是宏观感受), 其实他们是每个进程用一段时间的CPU(这个微观).
----- ------------ --------
----- --------- ---------
----- ---------- ---------
并发 : 多个进程使用多个CPU, 真正的同时执行.
函数调用
通过以上应该明白并行, 而我们实现的就是函数不停的被调用, 让人感受是不停切换. 现在我们就来着手分析一个函数调用的过程吧.
1. 从c语言角度来看
以下的例子来看, 函数调用时会先从main
跳转到fun
中区执行, 执行结束后在回到main
中执行剩下的指令.
void fun(int i)
{
}
int main()
{
fun(0);
exit(EXIT_SUCCESS);
}
2. 从汇编看
执行gcc -S func.c
将c代码转为汇编代码, 如果看不懂AT&T
汇编格式可以执行gcc -S -masm=intel func.c
转为intel
汇编就行了.
我将汇编代码简化一下, 只留重点.
main函数中执行
call
之指令, 调用fun
函数, 在函数执行之前现将rbp
(栈基址寄存器)压入栈中保存, 然后将函数栈的栈顶rsp
(栈顶寄存器)赋值给rbp
.我们传入了一个了参数, 可以看到
edi
存放的是参数的值, 将它存放在当前栈-4的位置, 这样就实现了函数参数的传递了.以上函数执行前的准备工作执行完后才开始真正的执行函数体中的操作, 但是这里我们什么都没有写, 所以指令只有一条
nop
(空指令).函数退出 : 将函数最开始压入栈中的
rbp
弹栈再赋值给rbp
, 这样rbp
又重新指向main
了.所以函数被调用前将
main
函数的地址压栈, 函数执行完后再将main
地址弹栈, 这样就实现一个完整的函数调用, 而且还能回到调用之前的函数.
rbp : 栈基址寄存器
rsp : 栈顶寄存器
fun:
.LFB2:
.cfi_startproc
pushq %rbp
movq %rsp, %rbp
movl %edi, -4%rbp ; 传入一个参数
nop
popq %rbp
ret
main:
.LFB3:
call fun
movl $0, %edi
call exit@PLT
3. 从编译器来看
这里做一个简单的了解就行了.
在编译期间, 编译器将每个文件声明函数名放在该文件的ELF中, 链接时将函数名与函数体关联链接, 当检测到相同函数名时编译器和链接器并不知道怎么处理, 所以相同声明的函数签名就会报错.
当没有冲突的函数时, 就会重定位ELF中函数的信息, 当检测到该函数被调用就会去查ELF表中该函数名是否存在, 如果存在再去定位函数所在的内存地址, 最后将定位到的地址替换函数名即可.
输入objdump a.out -x
命令你就可以看到该程序所有可用的头部信息, 包含符号表, 重定位入口. 我这里的fun
函数的信息是这样的, 存放在ELF中的.text
代码段中.
00000000000006b0 g F .text 0000000000000007 fun
小结
本节最重要的是明白汇编层次的函数调用是怎么实现的, 明白这些才容易理解下面我们所写线程调用的过程. 而汇编层次的函数调用涉及到的就是将rbp
(栈基址寄存器)压栈, 最后弹栈恢复现场就实现了函数调用.