现在的位置: 首页 > 综合 > 正文

fork,vfork以及exec的意义

2013年10月02日 ⁄ 综合 ⁄ 共 3800字 ⁄ 字号 评论关闭

linux中创建进程是直接的,就是简单的一个fork调用,linux认为进程就是执行的一个任务,并没有和可执行文件联系起来,如果非要和可执行文件 联系的话就要涉及到另一个系统调用exec.linux这么实现取得了很大的灵活性,这个灵活性实际上也是继承unix的,因为unix就是这样实现进程的,进程在unix或者类unix比如linux系统中就是一个执行续,而不是别的什么特殊的东西,这使得在这些系统中很容易就实现了线程的概念,而且可 以用统一的管理系统去管理线程或者进程,所不同的仅仅是进程拥有另外一些东西而线程却没有,线程成了进程的完全子集,这在设计上是很重要的.相 反,windows内核虽然完全实现了线程,但是它实现的线程却和进程的概念是完全分离的,唯一的相同点就是都是nt执行体的"可执行"对象,统一于对象 管理器,继承了可执行对象的一切特性.这样windows实现的现代进程和线程的概念就相当松散,不利于统一管理,这完全体现在用户接口上,我们看一下 windows的进程创建接口;
BOOL CreateProcess
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes。
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
这 里仅仅给出接口声明和简单的解释,更详细的请查阅MSDN,第一个参数是可执行模块的名称字符串,第二个是命令行,...就不多说了,看得晕吗?我是不喜 欢这种方式了,为什么进程一定要有可执行模块?去问微软吧!这也可能是微软的策略,故意让你进入他自己的开发模式,然后上瘾然后你就离不开微软了...; 再看一眼它的线程创建接口:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
    ); 
这个线程接口倒是比进程接口来的更直接,但是还是限制了很多东西,比如这回倒不必调用可执行模块了,但是为什么线程一定要有一个函数指针呢?
回归进程和线程的本意,进程是程序在计算机上的一次执行活动;线程是程序中一个单一的顺序控制流程。进程一定要有可执行模块吗?线程一定要有函数指针吗?都是不必的,可是提供给进程一 个可执行模块提供给线程一个函数指针会使得概念理解起来更容易,但是好处也仅仅如此。看看linux的方式吧,unix也同样。
   linux中的进程和线程接口一样,都是fork,并且没有参数,那怎么区分呢?linux提供了另一个系统调用接口clone,但是clone本质上也 是用的fork,就是多加了一个参数用来识别要创建的是一个线程。fork在内核中实现为sys_fork,clone的内核实现为sys_clone, 我们看一眼它们的形式:

asmlinkage int sys_fork(struct pt_regs regs)

{

         return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);

}

asmlinkage int sys_clone(struct pt_regs regs)

{

         unsigned long clone_flags;

         unsigned long newsp;

         int __user *parent_tidptr, *child_tidptr;

         clone_flags = regs.ebx;

         newsp = regs.ecx;

         parent_tidptr = (int __user *)regs.edx;

         child_tidptr = (int __user *)regs.edi;

         if (!newsp)

                 newsp = regs.esp;

         return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);

}

可 以看到二者都调用了do_fork,do_fork的代码就不分析了,这不是本文的主题,要注意的是do_fork的第一个参数是个标志,这个标志告诉内 核要创建的执行续是什么性质的,其中一个标志是CLONE_THREAD,就是要创建线程,而别的就是创建进程,当然标志是很丰富的,值得细细推敲。除了 标志没有任何限制,没有让用户提供什么别的东西,这其实是真正的进程/线程创建,试想有时候我仅仅想开辟一块内存然后让一个任务作为一个进程不被打扰得运 行,拥有自己的地址空间等等,而我没有关于这个任务的任何可执行文件。进程就是拥有独立的空间,是一个容器,而线程就是和别的同一进程的线程们共享该进程的空间和信号等等,至于说和可执行文件还有函数指针的联系是后来人们加上的,微软还真当成事儿了,当成规则标准了,这不是误人子弟吗?
那么linux要执行可执行文件该咋办呢?当然有办法了,不过要知道,linux认为一旦你执行了exec,你就是放弃了当前的进程地址空间而使用了新的地址空间来加载exec需要的可执行文件,这和有些文章说的地址空间蒸发不谋而合,但是不是蒸发了,而是被放弃了,仅仅是递减了该地址空间的引用计数而已,如果就一个task_struct使用了该mm_struct,那么它就被释放了,如果很多线程共享这个mm_struct,那么也就这个执行 exec的线程放弃了该mm_struct,对别的线程没有任何影响,可以看一下代码:

int do_execve(char * filename,

char __user *__user *argv,

         char __user *__user *envp,

struct pt_regs * regs)

{

...

         bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);

...

         bprm->file = file;

         bprm->filename = filename;

         bprm->interp = filename;

         bprm->mm = mm_alloc();  //bprm申请了一个mm_struct

...

         retval = search_binary_handler(bprm,regs);

...

}

我们看到专门为一个可执行的二进制文件申请了一个地址空间mm_struct,这个地址空间将来要替换当前进程的地址空间,我们马上看到这一幕,在search_binary_handler 最终要调用elf的加载函数load_elf_binary,在该函数里实现了替换,调用了retval = flush_old_exec(bprm)我们看一下这个非常的retval = flush_old_exec(bprm);

int flush_old_exec(struct linux_binprm * bprm)

{

...

         retval = exec_mmap(bprm->mm);  //这个函数最终实现了替换

...

         set_task_comm(current, tcomm);

         current->flags &= ~PF_RANDOMIZE;

         flush_thread();

         current->mm->task_size = TASK_SIZE;

...

         current->self_exec_id++;

         flush_signal_handlers(current, 0);

         flush_old_files(current->files);

...

}

在exec_mmap中有几个重要的操作实现了替换,我们看一下:
tsk = current;
old_mm = current->mm;
mm_release(tsk, old_mm);
active_mm = tsk->active_mm;
tsk->mm = mm;
tsk->active_mm = mm;
activate_mm(active_mm, mm);
这几个就不用多说了,这里的mm_release就是所谓的蒸发,希望大家通过这个对地址空间的蒸发知其所以然。
最后说一下另一个系统调用vfork,vfork不实现写时复制,而是阻塞当前调用线程,其间和被阻塞的进程公用地址空间,只有到子进程调用exec或exit时才唤醒被阻塞线程,这是为何呢?因为当子进程执行exec时,它放弃了地址空间,所以原线程就又可以用了

抱歉!评论已关闭.