0.进程与程序(process & program)
进程简单的讲就是运行中的程序,它是一个动态的实体,是程序的一次执行过程。进程与程序的区别在于进程是
动态的,程序是静态的,进程是运行中的程序,程序是一些保存在硬盘上的可执行的代码。所以很多人也说,进
程是活的,程序是死的。linux下可通过命令ps aux或top查看当前系统中的进程。
1.进程的内存映像
当一个程序执行时,操作系统将可执行程序复制到内存中,程序转化为进程要经过以下3个步骤:
*内核将程序读入内存,为程序分配内存空间;
*内核为该进程分配进程标识符(pid)和其他所需资源;
*内核为该进程保存pid级相应的状态信息,把进程放到运行队列中等待执行。程序转化为进程后就可以被操作系统
的调度程序调度执行了。
进程的内存映像指内核在内存中如何存放可执行程序文件。这里的可执行程序与内存映像的区别在于:
*可执行程序位于磁盘中,内存映像位于内存中;
*可执行程序没有堆栈,只有程序被加载到内存中才会分配堆栈;
*可执行程序是静态的、不变的,内存映像随着程序的执行是在动态变化的,因为数据是随着运行过程改变的。
2.fork 与vfork
linux下创建新进程的系统调用函数是fork,fork翻译成中文是“分叉”的意思,顾名思义,当一个进程运行时调用了fork函
数,那么它就会产生一个新的进程,也就是“分叉”了。
fork是一个很特殊的函数,特殊在它有两个返回值,也就是说,调用一次fork,返回两次。因为当我们成功调用fork之后,
当前进程已“分叉”为两个进程。一个是原来的父进程,另一个是刚刚创建的子进程。当调用失败时(父进程拥有的子进程
个数超过了规定限制,或者可供使用的内存不足),fork返回-1;成功时,对于父进程,返回的是子进程的pid,对于子
进程,返回的是0。(我们可以用getpid(),getppid()分别获取当前进程pid和当前进程父进程pid)。
一般来说,fork之后是父进程先执行还是子进程先执行是不确定的,这取决与内核所使用的调度算法。但因为操作系统一
般让所有进程都享有同等执行权,所以父子进程一般都是交替执行的。
当调用fork之后,系统让新进程与旧进程使用同一段代码,因为它们的程序还是相同的,同时其堆栈段数据段,系统会复
制一份给子进程,这样,子进程就几乎继承了父进程的全部属性,但是,子进程有它自己唯一的pid,而子进程一旦开
始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再
共享任何数据了。那么,如果一个程序很庞大,fork一次就复制一次,系统开销岂不是很大?其实,一般CPU都是以“页”为
单位而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段时,其实只是“逻辑”上的复制,并非“物理”上
的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,
这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开,这样系统在空间上的开销就可以达到最小
(写时拷贝)。
vfork也可以用来创建一个新进程,同样也是调用一次,返回两次。但注意,使用fork时,子进程完全复制父进程的资
源,但使用vfork创建的子进程共享父进程的地址空间,也就是说子进程完全运行在父进程的地址空间上。子进程对该
地址空间中任何数据的修改同样为父进程所见。同时,vfork保证子进程先运行,在子进程调用exit或exec之前父进程处
于阻塞等待状态。
为了区别fork与vfork,我们来看一个例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int globVar = 5;
int main(void)
{
pid_t pid;
int var = 1, i;
printf("fork is diffirent with vfrok \n");
// pid = fork();
pid = vfork();
switch(pid) {
case 0:
i = 3;
while(i-- > 0)
{
printf("Child process is running\n");
globVar++;
var++;
sleep(1);
}
printf("Child's globVar = %d,var = %d\n",globVar,var);
exit(0);
case -1:
perror("Process creation failed\n");
exit(0);
default:
i = 5;
while(i-- > 0)
{
printf("Parent process is running\n");
globVar++;
var++;
sleep(1);
}
printf("Parent's globVar = %d ,var = %d\n", globVar ,var);
exit(0);
}
}
上述程序,当我们使用fork时,结果如下:
当我们注释掉fork,使用vfork时,结果如下:
由此我们可以看出,fork继承了父进程的全局变量和局部变量,但fork的子进程有自己独立的地址空间,不管是
全局变量还是局部变量,子进程与父进程对他们的修改互不影响。vfork创建子进程后,父进程中的globvar和var
最后均递增了8,这是因为vfork的子进程共享父进程的地址空间,子进程修改变量对父进程是可见的。从运行结
果还可以看出,vfork保证了子进程先运行。
3.进程退出
*exit函数:exit(int exit_code),在stdlib.h中声明,exit中的参数若为0则代表正常终止,非0则异常终止。exit()
要先执行一些清除操作,然后将控制权交给内核。
*_exit函数: 在unistd.h中声明,执行后立即返回给内核。
*return:是函数执行完后的返回,将控制权交给调用函数。如果在main()中使用return语句,就相当于exit()。
*about:异常终止。
不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为进程关闭所有打开的文件描述符,释放它所使用的存储器等。
4.执行新程序
使用fork或vfork创建子进程后,子进程通常会调用exec函数来执行另外一个程序。也就是说系统调用exec函数用于执行
一个可执行程序以代替当前进程的执行映像。linux下,exec函数族有以下6中不同的调用形式。(声明在<unistd.h>
中)
int execl(const char *path, constchar *arg, ...)
int execv(const char *path, char *constargv[])
int execle(const char *path, const char *arg,..., char *const envp[])
int execve(const char *path, char *const argv[],char *const envp[])
int execlp(const char *file,const char *arg, ...)
int execvp(const char *file, char *constargv[])
注意:
exec调用并没有生成新进程,一个进程一旦调用exec函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,
废弃原有的数据段和堆栈段,并为新程序分配新的数据段和堆栈段,唯一保留的就是进程ID。也就是说,对系统而
言,还是同一个进程,不过执行的已经是另一个程序了。所以正常情况下,这些函数是不会返回的,因为进程的执行映
像已经被替换,没有接收返回值的地方了。
5.wait与waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int &status);
pid_t waitpid (pid_t pid, int *statloc, int options);
当子进程先于父进程退出时,如果父进程没有调用wait或waitpid函数,子进程就会进入僵死状态,如果父进程调用了
wait或waitpid函数,子进程就不会变成僵尸进程。
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果它找到了这样一
个子进程, wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻
塞在这里,直到有一个出现为止。参数status用来保存所指向的变量的存放子进程的退出码,它是一个指向int类型
的指针。但如果我们对这个子进程是如何死掉的毫不在意,只是想把这个进程消灭掉,我们就可以这样写:
pid = wait(NULL)。如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时
wait返回-1。
waitpid也用来等待子进程的结束,但它多了两个参数,这两个参数使他可以等待某个特定进程的结束。参数pid指明
要等待的子进程的pid。当pid>0时,只等待进程ID等于pid的子进程退出,不管其它已经有多少子进程运行结束退出了,
只要指定的子进程还没有结束,waitpid就会一直等下去;pid=-1时,等待任何一个子进程退出,没有任何限制,此时
waitpid和wait的作用一样。即wait(&status) 等价于waitpid(-1, &status, 0); pid=0时,等待同一个进程组中的任何子进
程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬; pid<-1时,等待一个进程组的ID等于pid的绝对值
的任一子进程退出。options参数目前在Linux中只支持WNOHANG和WUNTRACED两个选项,如果我们不想使用它
们,也可以把options设为0。有时希望取得一个子进程的状态,但不想使父进程阻塞,那么就可以使用WNOHANG参数
调用waitpid,表示即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。