第二章 进程的生成(1)
先说说什么是进程?假设你编好了一个程序,在它没有被调用之前,它只是乖乖地躺在你的硬盘上,什么事情都不干。好不容易编出来的不干活,这是我们不能容忍的。所以我们要把它调到内存里,然后通过CPU去执行它。所以说,进程就是一个在执行状态下的程序。
我们可以通过
- $ps –e
命令来查看一下,计算机里所运行的进程有哪些。
那我们计算机里这么多的进程的又是从哪里来的呢,我们可以通过
- $ps –axwf
来看到关于进程的一张家谱。其实,系统中的所有进程都是通过另一个进程生成的(除了0号进程以外),如果,A进程生成了B进程,那么可以把A进程叫做父进程,把B进程叫做A进程的子进程。也就是说,UNIX系统所有的进程都与其它进程保持着父子关系。
下面我们来看一个简单的例子
- #include<stdio.h>
- #include<sys/types.h>
- #include<unistd.h>
- int main( int argc , char *argv[])
- {
- int time;
- time = atoi( argv[1] )*60 ; //将参数中的分钟换成秒数
- if( fork()==0 ) //生成子进程,括号里是子进程的代码
- {
- sleep( time );
- fprintf( stderr , "it is time to alarm!/n");
- }
- return 0;
- }
执行时输入
- $./a.out 2
这是一个简单的闹钟程序。你把它执行后,看似系统没有什么反映,其实不是,在后台你已经生成了一个进程,来监视时间。如果你用ps命令查看就能看到它。这时你可以接着干你自己的事情。等到了你设定的时间之后这个进程会提示你时间已经到了。这个程序虽不完善(没有进行输入参数的检查),但可以简单的告诉大家如何生成一个进程。
为了生成一个新的进程,这里使用了 fork() 这个系统调用。它的作用是将父进程的各个变量的值复制给子进程,也就是说当你调用了fork()的那一刻,系统就为你生成了一个和父进程完全一样的进程。当然我们不想要一个父亲一样的孩子,孩子要有自己的个性,那我们如何来赋予孩子自己的个性呢?让我们先来看看fork()这个系统调用的概要。
- 头文件 #include <sys/types.h>
- #include <unistd.h>
- 形式 pid_t fork(void);
- 返回值 成功时: 父进程中:子进程的进程号(>0)
- 子进程中:=0
- 失败时: -1
根据上面fork()的特性,我们可以通过fork()的返回值区分父进程要做的事和子进程要做的事。例如,
- pid_t pid ;
- pid=fork();
- if( !pid)
- {
- //子进程要做的事
- }else if(pid >0)
- {
- //子进程生成失败时,父进程要做的事
- }else //pid<0
- {
- //子进程生成失败时,父进程要做的事
- }
好,我们现在已经学会了生成一个子进程了。但它还是遗传了许多父进程的特性,有可能大家会想,能不能用我们生成的子进程来执行另一个与父进程没有任何关系的程序呢。当然是可以的,比如我们常说的shell就是就是这个样子。shell本身也是一个进程当你输入命令回车以后,shell会生成一个子进程来执行你的命令,这条命令可以和shell没有丝毫关系。为了更好的说明问题我们先来做一个简单的shell。当然是最简单的那种。
- #include<stdio.h>
- #include<sys/types.h>
- #include<unistd.h>
- int main()
- {
- static char prompt[64]="> ";
- char command[256];
- int st;
- fprintf(stderr,"%s",prompt); // 屏幕上的输出提示符
- while(gets(command)!=NULL) // 取得键盘输入
- {
- if(fork()==0) // 生成子进程
- { // 子进程要做的事
- execl(command,command,(char *)0)==-1 //执行所输入的命令
- }
- else
- { // 父进程要做的事
- wait(&st); // 等待子进程结束
- fprintf(stderr,"%s",prompt); // 输出提示符,等待命令
- }
- }
- return 0;
- }
好了我们保存,编译,执行以下看看
- $./a.out
- >/bin/ls //这里必须输入命令的完全路径
- 当前目录下文件名
- >Ctrl+D 退出程序
- $
这样我们的一个最初级shell就做好了。虽然它还很弱,还有着安全上的漏洞(使用了gets()),甚至连自己退出都不能,但起码可以让我们看到一个shell是如何执行的了。其实,一个复杂的shell最基本的东西也就使这些。大家要是有兴趣的话可以将gets()换掉,再加上退出功能。
我们再说说程序中出现的一个新的函数execl()。其实它是exec函数组中的一个。这组函数有:
- int execl( path , arg0 , arg1 , ... , argn , (char *)0 );
- int execv( path , argv );
- int execle( path , arg0 , arg1 , ... , argn , (char *)0 , envp );
- int execve( path , argv , envp );
- int execlp( file , arg0 , arg1 , ... , argn , (char *)0 );
- int execvp( file , argv );
- 参数定义如下:
- char *path;
- char *file;
- char *arg0 , *arg1 , ... , *argn;
- char *argv[];
- char *envp[];
- 返回值: 成功时:所执行的命令将会覆盖现有的进程,所以无返回值
- 失败时:-1
比如说我们在shell里执行
- $ /bin/ls –l
这个命令,实际上shell调用的是
- execl( "/bin/ls" , "/bin/ls" , "-l" , (char *)0 );
的一个系统调用
这个函数组函数有6个,用法就不一一说明了,大家可以参看一下其它资料。这里只告诉大家它们的用处,exec函数组就是用来调用一个可执行程序。还有一点很重要,一但进程调用了exec函数那么写在exec函数后面的进程代码将会被覆盖,变成无效的代码了。
例如下面一段代码,我们想在execl执行后输出一段文字列,这是办不到的。
- if(fork()==0) // 生成子进程
- { // 子进程要做的事
- execl(command,command,(char *)0)==-1 //执行所输入的命令
- fprinf(stderr,"lalalalalalalalala!"); //
- }
- else
- { // 父进程要做的事
- wait(&st); // 等待子进程结束
- fprintf(stderr,"%s",prompt); // 输出提示符,等待命令
- }
还有一个函数wait(),它的概要是
- #include <sys/types.h>
- pid_t wait(int *status);
返回值就是子进程的进程号
它的参数是个指针。C语言里讲过,一个函数想有一个以上的返回值时,你可以将想返回的变量的地址作为函数的参数。比如说将数组地址作为函数的参数等等。其实这里的status就是这个道理,它的值与子进程的结束方式有关系。当你的子进程以exit()方式结束的话,status所指向的地址的前8位将会是exit()的参数的后8位,而status所指向的地址的后8位是0。例如子进程是exit(1);那status所指向的地址的内容应该是0000 0001 0000 0000。还有如果子进程是通过信号(signal)终止的(信号我们以后再讲),那么我们也可以通过status的值来判断是哪一个信号终止了这个子进程。(详见man)
我们为什么还要在父进程中调用wait(),这涉及到进程状态的概念,我们稍候再说。