fork和exec简介
在linux中启动新一个程序有两种方式:
- Fork(分叉)
- 创建一个新的进程,该进程几乎是前一个进程的拷贝
- 子进程和父进程使用相同的代码段,子进程复制父进程的堆栈段和数据段
- 子进程对父进程的复制采用 写时复制 的策略,所有实际上物理空间还是共享着的,直到一个进程写了数据,才将有差别的”页”从物理地址上分开
- Exec
- 启动另外的进程以取代当前运行的进程
- 调用exec的进程将会死亡,废弃原有的数据段和堆栈段,分配新的数据段和堆栈段,但是进程号是同一个 。所以对于系统来说还是同一个进程,但不是同一个程序了
结论:
fork时将父进程的虚拟空间复制给子进程一份,但此时父子进程使用的是同一块物理空间。当对这块共享地址读写时,才为子进程生成一块独立的不同于父进程的物理空间,称为写时复制(copy on write, cow)。当然cow后子进程虚拟空间不变与fork时父进程的虚拟空间相同,正文段,数据段,堆,栈这四个部分内容相同(即变量地址、函数地址等与父进程相同)。也因此父子进程存在 地址相同但值不同 的情况,因为用户看到的是进程的虚地址。
fork, vfork, clone的区别与联系
- fork
- fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容
task_struct
内容 - 子进程会复制父进程的
task_struct
,系统堆栈和页表
- fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容
- vfork
- vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行
- vfork创造出来的是轻量级进程,也叫线程,是共享资源的进程
- clone
- 可以通过传递不同的参数来选择性的复制父进程的资源
写时复制(copy on write, COW)
当子进程改变了变量时,才会通过写时复制的方法为涉及的页表建立新的副本。即当子进程或则进程尝试写一个共享的页帧时就产生一个异常(page_fault
, int14
)中断,此时CPU会执行相应的异常处理函数do_wp_page()
。
do_wp_page()
会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一新的物理页面,使父进程和子进程各自拥有一块内容相同的物理页面。最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去。
需要注意的是,每个进程有自己独立的虚拟地址,复制后子进程段、变量的地址空间(虚地址空间)和父进程是一样的,但是映射到的物理空间不同,所以存在 地址相同但值不同 的情况。因为用户看到的是进程的虚地址。
fork函数的执行过程
- 触发
sys_clone
系统调用 - 执行
do_fork()
- 执行
copy_process()
- 执行
dup_task_struct()
,copy_file()
,copy_thread()
,copy_mm
等
- 执行
wake_up_new_task(task_struct p)
put_pid(struct pid)
- 执行
- 执行
ret_from_fork()
实战
- 一般使用
fork()
函数新建一个进程,然后让进程去执行exec
调用- 由于写时复制策略,
fork()
后会优先执行子进程(当然有时也不会),如果子进程直接exec
将可执行文件载入地址空间开始执行,它就无需赋值了,效率提高 - 如果不先执行子进程,则父进程很可能会产生写操作导致复制开始
- 由于写时复制策略,
- 在命令中修改文件描述符时(如重定向,关闭fd等),命令执行结束时描述符就会自动还原。可以通过
exec
执行- 如
exec 2>/dev/null
,那么直到shell退出,2(标准错误)都一直会为/dev/null
- 如