神奇的vfork
来源:互联网 发布:三星pc软件手机版本 编辑:程序博客网 时间:2024/04/30 15:22
http://hi.baidu.com/_kouu/blog/item/3e92640e3b6393e4ab645784.html
一段神奇的代码
在论坛里看到下面一段代码:
int createproc();
int main()
{
pid_t pid=createproc();
printf("%d\n", pid);
exit(0);
}
int createproc()
{
pid_t pid;
if(!(pid=vfork())) {
printf("child proc:%d\n", pid);
return pid;
}
else return -1;
}
输出结果:
child proc:0
0
child proc:0
Killed
感觉非常奇怪,为什么vfork以后,父子进程都走了“子进程”的分支呢?
什么是vfork?
什么是vfork,网络上介绍它的文档很多,随便一搜就是一大堆。简单来说,vfork和fork完成了基本上相同的功能,把进程做了一次复制,变成两个进程。
在shell中,执行命令时,shell程序就是通过“复制”形成了父子进程。子进程生成后,执行exec系列函数,载入新的可执行文件,开始执行。
由于复制完成后,子进程马上就要载入新的程序来运行了,在此之前从父进程那里复制来的内存空间都不需要了。所以,“复制”过程中,复制内存空间是件费力不讨好的事情。
所以,fork有了“写时复制”技术。“复制”的时候内存并没有被复制,而是共享的。直到父子进程之一去写某块内存时,它才被复制。(内核先将这些内存设为只读,当它们被写时,CPU出现访存异常。内核捕捉异常,复制空间,并改属性为可写。)
上面说到的内存空间是实际存储用户数据的空间,利用“写时复制”避免了干前面提到的那件费力不讨好的事情。
但是,“写时复制”其实还是有复制,进程的mm结构、页表都还是被复制了(“写时复制”也必须由这些信息来支撑。否则内核捕捉到CPU访存异常,怎么区分这是“写时复制”引起的,还是真正的越权访问呢?)。
而vfork就把事情做绝了,所有有关于内存的东西都不复制了,父子进程的内存是完全共享的。但是这样一来又有问题了,虽然用户程序可以设计很多方法来避免父子进程间的访存冲突。但是关键的一点,父子进程共用着栈,这可不由用户程序控制的。一个进程进行了关于函数调用或返回的操作,则另一个进程的调用栈(实际上就是同一个栈)也被影响了。这样的程序没法运行下去。
所以,vfork有个限制,子进程生成后,父进程在vfork中被内核挂起,直到子进程有了自己的内存空间(exec**)或退出(_exit)。并且,在此之前,子进程不能从调用vfork的函数中返回(同时,不能修改栈上变量、不能继续调用除_exit或exec系列之外的函数,否则父进程的数据可能被改写)。
尽管限制很多,但并不妨碍实现前面提到的关于shell程序的那个“需求”。
问题的思考
说到这里,可以看出文章开头的那段代码是存在问题的了。子进程不但调用了printf,还从createproc函数中返回了。
但是,子进程的违规为什么会使父进程走上“child proc”这条路呢?父进程在子进程退出前被阻塞在vfork里面,vfork的返回值是如何变成0的呢?
前面一直在说vfork,其实它是两个东西,库(libc)函数vfork和系统调用vfork。用户程序调用的是库函数,而库函数再去调用系统调用。用户程序中几乎所有的系统调用都是通过库函数去调用的。因为不同体系结构下(甚至相同体系结构),系统调用的指令和参数传递规则都可能不同,这些细节被库函数隐藏了。
前面提到,父进程被挂起在vfork中,这是指的系统调用vfork。在系统调用中,进程使用的是内核栈(每个进程有着自己独有的内核栈)。此时,父进程在内核里面是安全的,随便子进程怎么违规。内核会保证系统调用vfork的完整性,系统调用的返回值也不会有问题(它是通过寄存器传回用户空间的,跟栈无关)。
而vfork的返回值变成0的问题,则是在库函数vfork中产生的。既然子进程已经违规了,库函数没办法保证程序的正确性。而库函数vfork是否返回0也是不确定的,可能不同版本的libc、不同的程序上下文、不同的系统、等等、都会有不同的返回值(或者就直接“段错误”了)。还有可能是,父进程中库函数vfork并没有返回0,但是栈上的返回地址被改写了,从函数createproc返回,返回到printf("childproc")这句话去了。
再深入一点
vfork后,库函数没法保证子进程在进行函数调用或返回的操作后程序还正常,但是库函数vfork本身就是一个函数呀,从系统调用vfork返回后,库函数vfork接着又返回了。这时,程序的正确性又是如何保证的呢?
关于函数调用,一般而言:调用前-调用者将需要传递的参数放到栈上;调用时-调用者使用call指令,该指令自动将返回地址入栈;调用后,在被调用的函数中,第一件事是做调用栈的调整,如createproc函数如是做:
08048487:
8048487: 55 push %ebp
8048488: 89 e5 mov %esp,%ebp
804848a: 83 ec 28 sub $0x28,%esp
......
其中ESP是当前栈的指针,而EBP是上一层调用栈的指针。调用栈调整之前,EBP保存着上上一层栈的指针,这个值不能丢,需要放在栈上,以便函数返回时恢复。
每层调用都有自己的调用栈,“深”的调用不会影响到之前的调用栈。所以,vfork后子进程调用其他函数应该是没有问题的(但是可能会改写掉属于父进程的某些数据,造成逻辑上的错误),只要它不从调用vfork的函数中返回就行了。
但是,库函数vfork本身却不是这样做的。在这个函数中没有使用栈上的内存空间,它没有去进行调用栈的切换,如:
000983f0 <__vfork>:
983f0: 59 pop %ecx
983f1: 65 8b 15 6c 00 00 00 mov %gs:0x6c,%edx
983f8: 89 d0 mov %edx,%eax
983fa: f7 d8 neg %eax
......
9840e: cd 80 int $0x80
98410: 51 push %ecx
......
所以父进程在库函数中运行时,不用担心栈上的数据已经被子进程修改(它根本不去使用栈上的数据)。
然而call/ret指令却不得不使用栈(因为返回地址自动会被CPU放在栈上),如果子进程在vfork后调用其他函数,会使得父进程在进入库函数vfork时通过call指令在栈上留下的“返回地址”被擦掉。
事情的确是这样。于是库函数vfork为了解决这个问题,做了一些手脚,它并没有让栈上的“返回地址”一直留在栈上。注意上面的汇编代码,进入库函数vfork的第一条指令就是“pop %ecx”,把放在栈上的“返回地址”弹到了ECX中去,保存起来。然后在系统调用vfork返回后(int0x80是用于系统调用的指令),再“push %ecx”,把“返回地址”放回去。
•关于vfork不会改变局部数据的问... •fork,vfork和clone---转•linux 父子进程共享区域与缓冲(f... •fork vfork exec wait使用和区别•fork与vfork的深入理解 •linux系统调用fork vfork clone•fork 和 vfork •fork()/vfork()与exit()/_exit()•fork函数与vfork函数 •进程相关函数 fork() vfork() e...更多>>
一段神奇的代码
在论坛里看到下面一段代码:
int createproc();
int main()
{
pid_t pid=createproc();
printf("%d\n", pid);
exit(0);
}
int createproc()
{
pid_t pid;
if(!(pid=vfork())) {
printf("child proc:%d\n", pid);
return pid;
}
else return -1;
}
输出结果:
child proc:0
0
child proc:0
Killed
感觉非常奇怪,为什么vfork以后,父子进程都走了“子进程”的分支呢?
什么是vfork?
什么是vfork,网络上介绍它的文档很多,随便一搜就是一大堆。简单来说,vfork和fork完成了基本上相同的功能,把进程做了一次复制,变成两个进程。
在shell中,执行命令时,shell程序就是通过“复制”形成了父子进程。子进程生成后,执行exec系列函数,载入新的可执行文件,开始执行。
由于复制完成后,子进程马上就要载入新的程序来运行了,在此之前从父进程那里复制来的内存空间都不需要了。所以,“复制”过程中,复制内存空间是件费力不讨好的事情。
所以,fork有了“写时复制”技术。“复制”的时候内存并没有被复制,而是共享的。直到父子进程之一去写某块内存时,它才被复制。(内核先将这些内存设为只读,当它们被写时,CPU出现访存异常。内核捕捉异常,复制空间,并改属性为可写。)
上面说到的内存空间是实际存储用户数据的空间,利用“写时复制”避免了干前面提到的那件费力不讨好的事情。
但是,“写时复制”其实还是有复制,进程的mm结构、页表都还是被复制了(“写时复制”也必须由这些信息来支撑。否则内核捕捉到CPU访存异常,怎么区分这是“写时复制”引起的,还是真正的越权访问呢?)。
而vfork就把事情做绝了,所有有关于内存的东西都不复制了,父子进程的内存是完全共享的。但是这样一来又有问题了,虽然用户程序可以设计很多方法来避免父子进程间的访存冲突。但是关键的一点,父子进程共用着栈,这可不由用户程序控制的。一个进程进行了关于函数调用或返回的操作,则另一个进程的调用栈(实际上就是同一个栈)也被影响了。这样的程序没法运行下去。
所以,vfork有个限制,子进程生成后,父进程在vfork中被内核挂起,直到子进程有了自己的内存空间(exec**)或退出(_exit)。并且,在此之前,子进程不能从调用vfork的函数中返回(同时,不能修改栈上变量、不能继续调用除_exit或exec系列之外的函数,否则父进程的数据可能被改写)。
尽管限制很多,但并不妨碍实现前面提到的关于shell程序的那个“需求”。
问题的思考
说到这里,可以看出文章开头的那段代码是存在问题的了。子进程不但调用了printf,还从createproc函数中返回了。
但是,子进程的违规为什么会使父进程走上“child proc”这条路呢?父进程在子进程退出前被阻塞在vfork里面,vfork的返回值是如何变成0的呢?
前面一直在说vfork,其实它是两个东西,库(libc)函数vfork和系统调用vfork。用户程序调用的是库函数,而库函数再去调用系统调用。用户程序中几乎所有的系统调用都是通过库函数去调用的。因为不同体系结构下(甚至相同体系结构),系统调用的指令和参数传递规则都可能不同,这些细节被库函数隐藏了。
前面提到,父进程被挂起在vfork中,这是指的系统调用vfork。在系统调用中,进程使用的是内核栈(每个进程有着自己独有的内核栈)。此时,父进程在内核里面是安全的,随便子进程怎么违规。内核会保证系统调用vfork的完整性,系统调用的返回值也不会有问题(它是通过寄存器传回用户空间的,跟栈无关)。
而vfork的返回值变成0的问题,则是在库函数vfork中产生的。既然子进程已经违规了,库函数没办法保证程序的正确性。而库函数vfork是否返回0也是不确定的,可能不同版本的libc、不同的程序上下文、不同的系统、等等、都会有不同的返回值(或者就直接“段错误”了)。还有可能是,父进程中库函数vfork并没有返回0,但是栈上的返回地址被改写了,从函数createproc返回,返回到printf("childproc")这句话去了。
再深入一点
vfork后,库函数没法保证子进程在进行函数调用或返回的操作后程序还正常,但是库函数vfork本身就是一个函数呀,从系统调用vfork返回后,库函数vfork接着又返回了。这时,程序的正确性又是如何保证的呢?
关于函数调用,一般而言:调用前-调用者将需要传递的参数放到栈上;调用时-调用者使用call指令,该指令自动将返回地址入栈;调用后,在被调用的函数中,第一件事是做调用栈的调整,如createproc函数如是做:
08048487
8048487: 55 push %ebp
8048488: 89 e5 mov %esp,%ebp
804848a: 83 ec 28 sub $0x28,%esp
......
其中ESP是当前栈的指针,而EBP是上一层调用栈的指针。调用栈调整之前,EBP保存着上上一层栈的指针,这个值不能丢,需要放在栈上,以便函数返回时恢复。
每层调用都有自己的调用栈,“深”的调用不会影响到之前的调用栈。所以,vfork后子进程调用其他函数应该是没有问题的(但是可能会改写掉属于父进程的某些数据,造成逻辑上的错误),只要它不从调用vfork的函数中返回就行了。
但是,库函数vfork本身却不是这样做的。在这个函数中没有使用栈上的内存空间,它没有去进行调用栈的切换,如:
000983f0 <__vfork>:
983f0: 59 pop %ecx
983f1: 65 8b 15 6c 00 00 00 mov %gs:0x6c,%edx
983f8: 89 d0 mov %edx,%eax
983fa: f7 d8 neg %eax
......
9840e: cd 80 int $0x80
98410: 51 push %ecx
......
所以父进程在库函数中运行时,不用担心栈上的数据已经被子进程修改(它根本不去使用栈上的数据)。
然而call/ret指令却不得不使用栈(因为返回地址自动会被CPU放在栈上),如果子进程在vfork后调用其他函数,会使得父进程在进入库函数vfork时通过call指令在栈上留下的“返回地址”被擦掉。
事情的确是这样。于是库函数vfork为了解决这个问题,做了一些手脚,它并没有让栈上的“返回地址”一直留在栈上。注意上面的汇编代码,进入库函数vfork的第一条指令就是“pop %ecx”,把放在栈上的“返回地址”弹到了ECX中去,保存起来。然后在系统调用vfork返回后(int0x80是用于系统调用的指令),再“push %ecx”,把“返回地址”放回去。
•关于vfork不会改变局部数据的问... •fork,vfork和clone---转•linux 父子进程共享区域与缓冲(f... •fork vfork exec wait使用和区别•fork与vfork的深入理解 •linux系统调用fork vfork clone•fork 和 vfork •fork()/vfork()与exit()/_exit()•fork函数与vfork函数 •进程相关函数 fork() vfork() e...更多>>
0
上一篇:浅析:setsockopt()改善socket网络程序的健壮性
下一篇:CentOS5.5 FTP安装配置
相关热门文章
- 欢迎神奇宝贝的蓝军在ChinaUni...
- 欢迎一杯神奇的可乐在ChinaUni...
- 欢迎神奇的咖啡屋在ChinaUnix...
- 欢迎神奇变牌手法揭秘在ChinaU...
- 欢迎飞天神奇在ChinaUnix博客...
- test123
- 编写安全代码——小心有符号数...
- 使用openssl api进行加密解密...
- 一段自己打印自己的c程序...
- sql relay的c++接口
- linux dhcp peizhi roc
- 关于Unix文件的软链接
- 求教这个命令什么意思,我是新...
- sed -e "/grep/d" 是什么意思...
- 谁能够帮我解决LINUX 2.6 10...
给主人留下些什么吧!~~
评论热议
0 0
- 神奇的vfork
- 神奇的vfork
- 神奇的vfork
- 神奇的vfork()
- 神奇的vfork()
- 神奇的vfork
- 神奇的vfork
- 神奇的vfork
- 神奇的vfork
- vfork的错误用法
- vfork()的体会
- vfork()的若干问题
- fork, vfork的区别
- vfork()的若干问题
- fork /vfork 的异同
- vfork的那些事情
- vfork
- vfork
- linux网络编程函数解析(1)--setsockopt
- 网络编程之setsockopt
- 浅析:setsockopt()改善程序的健壮性
- Linux bashrc和profile的用途和区别
- 浅析:setsockopt()改善socket网络程序的健壮性
- 神奇的vfork
- linux 下处理bom问题
- Kafka深度解析
- Oracle存储过程
- Docker科普
- Android Service学习总结(下)
- 以前的东西整理(5)
- 跟着Google学Android —— 3.1 管好Activity的生命周期
- 2016-AspNet-MVC教学-7-Linq在数组中的应用
原创粉丝点击
热门IT博客
热门问题
老师的惩罚
人脸识别
我在镇武司摸鱼那些年
重生之率土为王
我在大康的咸鱼生活
盘龙之生命进化
天生仙种
凡人之先天五行
春回大明朝
姑娘不必设防,我是瞎子
牛不排便不倒嚼怎么办
牛不吃草涨肚怎么办
牛吃玉米吃撑了怎么办
吃了块鸡骨头怎么办
误吞了鸡骨头怎么办
1岁宝宝积食发烧怎么办
3岁宝宝积食发烧怎么办
4个月宝宝积食怎么办
一岁宝宝积食了怎么办
受凉了咽喉痛该怎么办
鱼刺卡在喉咙里怎么办?
小鱼刺卡在喉咙怎么办?
手被刀割下肉了怎么办
膀胱镜做完尿痛怎么办
孕38周羊水偏多怎么办
孕妇8个月羊水多怎么办
孕40周羊水偏多怎么办
七个月羊水偏多怎么办
孕35周羊水偏多怎么办
24周羊水偏少怎么办
孕23周羊水多怎么办
孕32周羊水25.4怎么办
孕32周羊水19.1怎么办
怀孕7个月羊水多怎么办
9个月孕妇羊水多怎么办
怀孕31周羊水少怎么办
孕妇23周羊水多怎么办
孕38周羊水浑浊怎么办
怀孕3个月羊水少怎么办
怀孕9个月羊水少怎么办
怀孕6个月羊水少怎么办
怀孕8个月羊水少怎么办
怀孕8个月羊水多怎么办
9个月孕妇羊水少怎么办
羊水76少怎么办8个多月
孕妇8个月羊水少怎么办
孕妇7个月羊水少怎么办
怀孕38周羊水偏少怎么办
怀孕36周羊水398怎么办
怀孕28周羊水少怎么办
孕29周羊水略多怎么办