linux进程控制(一)--unix环境高级编程读书笔记

     1.进程PID和特殊的3个进程

     每一个进程在系统中都有一个唯一的标识,这个标识叫做进程标识符,或者叫  PID(process identity)。我们可以通过调用  getpid  函数来获取一个进程的PID,也可以调用getppid函数来获取当前进程的父进程PID。在  linux  系统中,有三个特殊的进程,它们的进程  PID  分别为0,1,2。0号进程是系统进程,它存在与内核当中,而不是磁盘文件上。该进程常被成为交换进程,或者叫  swapper。1号进程就是  init  进程,这个进程用来读取一些初始化文件,并且引导系统到一个状态,如多用户状态,init  进程是一个用户进程。2号进程是页精灵进程,用来做一些和虚拟内存相关的工作,可是这个进程我没有在系统中找到。如果有知道的朋友,请告诉我,这个页精灵进程在系统中是哪一个进程,另外页精灵进程也是系统进程。需要说明的是,这3个进程都是由特殊的方式产生的,除了这3个进程以外,linux  系统中的其它进程都是由调用  fork  函数产生的。

     2.fork函数

     fork  函数用来创建一个子进程,这是  linux  系统中创建子进程的唯一方式。当调用  fork  函数之后,我们称调用  fork  函数产生的进程为子进程,调用fork函数的进程为父进程。fork  函数调用一次,但是返回两次。这两次返回分别是在父进程和子进程中,在父进程中返回子进程的  PID,在子进程中返回0。当调用  fork  函数成功后,父子进程就分别从调用  fork  的地方开始执行。子进程是父进程的一个拷贝,但是现在很多的实现,并不是调用  fork  成功之后就立即拷贝,而是采用了一种称为  COW(copy on write)的技术,只有当子进程需要修改一些值时,才进行拷贝。拷贝的是父进程的数据空间以及堆和栈。我们可以用一个小例子来解释一下  fork  函数的作用及特性:

#include <stdio.h>
#include <unistd.h>

int glob = 6;
char buf[] = "a write to stdout\n";

int main(void)
{
        pid_t pid;
        int var = 88;

        if( write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1 )
        {
                printf("write error\n");
                return -1;
        }
        printf("before fork\n");

        if( (pid=fork())<0 )
        {
                printf("fork error\n");
                return -1;
        }
        else if(pid==0)
        {
                glob++;
                var++;
        }
        else
        {
                sleep(2);
        }

        printf("pid=%d\tglob=%d\tvar=%d\n",getpid(),glob,var);

        return 0;
}

将程序编译生成  a.out  直接运行,输入命令:
./a.out

得到输出结果如下:
a write to stdout
before fork
pid=3131	glob=7	var=89
pid=3130	glob=6	var=88

如果将程序的输出结果重定向到一个文件:
./a.out > /tmp/a

查看  /tmp/a  的内容:
vim /tmp/a

得到输出结果如下:
a write to stdout
before fork
pid=3077        glob=7  var=89
before fork
pid=3076        glob=6  var=88

     看到这个结果,我们可能会感到奇怪。为什么第一次输出了一次  before fork,而后面重定向到文件之后,就输出了两次呢?原因其实很简单,我们之前说过,fork之后的子进程会复制父进程的数据空间和堆栈。因为  write  函数是系统调用,没有缓存,所以立即就输出了。而printf在交互的情况下是行缓存的,它是由换行符刷新的,所以当没有重定向时,printf  碰到换行符就立即输出了。而当把这个程序重定向到一个文件时,printf  函数就变为全缓存的了。所以它并不立即输出,而是保留在缓存中。在调用  fork  之后,当子进程复制父进程时,把父进程缓存中的内容也复制到子进程的工作空间中。所以  before fork  分别在父进程和子进程中各输出一次。

     fork  函数创建的子进程,会把在父进程中打开的文件描述符,复制到子进程中,也就是说它们共享同一个文件表项。

     3.vfork函数

     vfork  函数和  fork  函数的基本功能是一样的,它也是调用一次返回两次。但是  vfork  函数和  fork  函数的不同在于,vfork  的目的是创建一个新进程,该进程用来通过调用  exec  函数来执行一个新进程,所以子进程并不复制父进程的数据空间和堆栈,而是先在父进程的工作空间中运行。vfork  函数要求父进程等待子进程先执行,如果在调用了vfork函数之后,子进程的执行还需要依赖于父进程的进一步动作,这将导致死锁。只有当子进程执行了  exec  函数或者  exit  函数之后,父进程才可以被调度执行。我们将上面的程序中的fork函数替换为vfork函数,得到的程序如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int glob = 6;

int main(void)
{
        int var = 88;
        pid_t pid;
        printf("before vfork\n");

        if( (pid=vfork())<0 )
        {
                printf("vfork error\n");
                return -1;
        }
        else if(pid==0)
        {
                var++;
                glob++;
                _exit(0);
        }

        printf("pid=%d var=%d glob=%d\n",getpid(),var,glob);
        return 0;
}

     不论你将上面的程序执行多少次,
before vfork
pid=3487 var=89 glob=7

输出结果肯定是上面的样子,因为  vfork  函数保证子进程先执行。在子进程中修改了  var  和  glob  的值之后,就调用了  _exi  t函数退出了。因为vfork函数产生的子进程并不复制父进程的工作空间,它是在父进程的工作空间中运行的,所以子进程改变了父进程工作空间中的数据,这是很正常的。

     4.init  进程领养其他进程

     经过前面的介绍,我们已经了解了  init  进程。它不是一个系统进程,它就是存在于  /sbin/init  文件。它的作用是在系统启动时用来读取一些初始化信息,将系统引导到一个状态。另外,它还有一个重要的角色是收养系统中的孤儿进程。那么什么进程叫做孤儿进程呢?顾名思义,孤儿进程就是其父进程在其终止之前终止的进程。

     init  进程收养孤儿进程的手续是怎么样的呢?当一个进程要终止时,内核会逐个检查系统中的各个进程,查看它们的父进程是否是正要终止的进程,如果是这样的话,就将它们的父进程修改为  init  进程。

     另外,关于init进程清除系统中的僵死进程和僵死进程的一些概念,请参考博客点击打开链接。

     5.wait和waitpid

     wait  和  waitpid  函数,用来在父进程中等待子进程的结束。wait  函数用来等待父进程的任何一个子进程,只要有子进程终止,它就立即返回。调用  wait  函数,会发生3种情况:

1.当该进程没有任何子进程时,会发生错误,返回-1
2.当该进程的子进程都在运行,尚没有子进程结束时,该进程会发生阻塞
3.当该进程已经有子进程终止时,可期待  wait  函数立即返回,返回值为终止子进程  PID

waitpid  函数和  wait  函数的不同在于,通过参数的设置,它既可以等待任何一个子进程(这时候功能相当于  wait),又可以等待指定  pid  的子进程,并且它可以不用阻塞。

     6.exec函数

     exec  函数族包括很多的函数,这些函数的作用都是执行一个新的进程。内核执行一个进程的唯一方法是调用  exec  函数,但是  exec  函数并不修改进程的PID。一般的使用方法是先调用fork函数创建一个子进程,然后在子进程中执行  exec  函数。exec  函数族如下:

       #include <unistd.h>

       extern char **environ;

       int execl(const char *path, const char *arg, ...);
       int execlp(const char *file, const char *arg, ...);
       int execle(const char *path, const char *arg,
                  ..., char * const envp[]);
       int execv(const char *path, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execve(const char *filename, char *const argv[],
                  char *const envp[]);



这六个函数的记忆方法是以  p  结尾的  execlp  和  execvp  中第一个参数是文件名,而不是路径。当执行这个文件名指定的文件时,就会在  PATH  指定的路径中,搜寻指定的文件。如果在文件名中出现“/”则按路径来处理。名字中包含“l” 的函数  execl ,execlp 和  execle,说明了在这些函数中传递给要执行的程序的命令行参数是一个列表,这个列表以空指针NULL结尾,它们参数排列一般如下:
pathname , arg1 , arg2 , (char*)0

而函数名字中带有v的函数  execv, execvp 和 execve,一般是将命令行参数的指针放在一个数组  argv  中,这个数组以空指针  NULL  结尾。函数名中包含“e”的函数  execve  和  execle,它规定了需要向该函数传递环境变量。

     上面的6个函数中只有  execve  是系统调用,其他的函数都是库函数,最终都需要调用  execve  函数。

你可能感兴趣的:(linux,进程,fork,exec,unix高级编程)