linux fork 函数,Linux的fork()系统调用

Linux的fork()系统调用,就是以父进程为模版创建子进程,是Linux系统的进程管理机制的核心API之一,另一个是调度器函数schedule(),它的用户态API就是之前说自旋锁时提到的sched_yield()。

如果是“21天学写操作系统”,那么最先要实现的就是这两个API。实现了这两个就可以在bochs虚拟机上跑个多进程demo了。

先看fork的man手册介绍:

286c89ef2b23dd1c2f70eb3a9422c60e.png

就是创建子进程,并且返回pid进程号。它的特点是fork一次返回两次,子进程返回0,父进程返回子进程的pid。

Linux有getpid()、getppid()函数,子进程可以使用后者获得父进程的进程号,但父进程可能有多个子进程,所以用fork返回值表示最近创建的子进程号。

它的常见用法就是:

pid_t cpid = fork();

if (-1 == cpid) {

//fork失败,一般是内存不够了

} else if (0 == cpid) {

//子进程代码

} else {

//父进程代码

}

类似的代码在很多介绍fork的书上出现,代码流程在fork之后根据它的返回值是否为0分成了两个分支,就像娜迦海妖的叉状闪电(forked lightning)一样,程序在这里分成了两个分支:(

太极生两仪,这是属于Linus大神的黑魔法之一,我们接下来窥探一下。

e8ac039c87a2f9e1d969a7c730e56338.png

如上图,首先要定义一个task_t的结构来代表“进程”,我们这种示例代码只需要设置3个值就行,“进程号”id,栈指针rsp,指令指针rip。task 的成员变量与x64同名寄存器一样,用来在“进程切换”时保存这两个关键信息。

我们只支持2个进程,进程的task_t大小为4096字节,一个内存页。它的低地址是task_t数据结构,高地址是“进程的栈”。

t_fork()函数的前半部分是获取一个task_t结构,然后用memcpy()函数把父进程的task_t拷贝到子进程,完成“子进程的复制”。

子进程与父进程不同的只有3项,即id,栈指针rsp,启动地址rip。

子进程是初次执行,模拟从fork()调用中返回,所以它的启动地址rip设置为do_fork_first汇编函数。

然后设置它的栈指针rsp,这个需要计算,方法就是把当前的栈指针(隶属于父进程)在父进程的task_t里的偏移量,加在子进程里就行。

子进程是父进程的拷贝,除了task_t不一样,数据部分是一样的。

算法:child>rsp = child + parent->rsp - parent,当前的rsp寄存器的值就是父进程的栈指针。

因为要获取寄存器rsp的值,这个计算也放在do_fork()汇编函数里,就几行代码。

子进程的进程号作为最后一个参数传给do_fork(),然后会被do_fork()直接作为返回值,这样父进程的t_fork()就会返回子进程的进程号。

即父进程返回子进程的进程号。

这时子进程还没有运行,它要等到调度器去调度它,即父进程执行t_schedule()函数时。

如下图,t_schedule()函数直接调用了汇编函数switch_to(),把当前的进程t_prev切换到下一个进程t_next。

切换的关键数据就是task_t的rsp、rip项,保存当前进程的这两项,加载下一个进程的这两项。

cd9d23269ff4fb58aacccde6d0f246d1.png

上图是fork用法的示例代码,else if分支是子进程1,最底部的while是父进程0。

8c1f45fd854965cb48824f1d07dc0b6d.png

上图是main()函数,初始化进程管理结构,相当于在内核态。最后它手工设置一个0号进程,把自己降低到用户态。

只有这一个进程是手工设置的,其他都是fork()的。

task_t必须在main函数的栈上,否则修改了栈指针之后操作系统不允许压栈,毕竟我们不是“真内核”,没那么大的权限。

那么,我们就在main()里开一个大的局部变量的数组,用来存放task_t结构,把它的高地址作为“进程栈”。

t0_run()的代码必须要少,以防调用层级过多,导致栈增长时覆盖了低地址的task_t关键数据。

在Linux内核里编程也有这个特点,在栈上不能申请大的局部变量空间,因为栈是有限的,早期是4k之后是8k,一旦覆盖了低地址的task_struct关键数据,就挂了。

下图的switch_to()函数是进程切换的重点,它在调度器的调度算法选择完要切换的进程之后,具体执行进程切换。

我们就模拟了两个,自然也没啥调度算法选择了,只能0切换1,1切换0。

1,保存rbp寄存器。

保存图中L1标号的地址,x64没法直接mov两个内存变量,必须拿一个寄存器当中介,这里是rbp。

保存当前的rsp寄存器。

这些操作都是在prev进程的栈上。

2,movq %rdx, %rsp,这行切换寄存器rsp为next进程的栈,它之前作为函数的实参保存在rdx里。

从这开始,操作切换到了next进程的栈上。

也没啥操作了,直接跳转next进程的rip指令位置执行就行,它也作为函数形参保存在rcx里。

如果next进程不是第一次执行,这个地址就是标号L1,因为我们上次把它切换出去时保存的就是这个地址。

如果next进程是第一次执行,这个地址就是do_fork_first()函数的地址,因为我们在t_fork()里设置的就是这个地址。

do_fork_first()就是把表示返回值的rax寄存器设置为0,然后返回。这样子进程“从fork()返回”时的返回值就是0了。

我们这个例子是用户态的进程,即协程。

实际内核里进程切换,要复杂一些,还有页表切换之类的。

81b2fa5a9391c1e8649d15255fbe23ea.png

举报/反馈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值