进程控制(1)

进程控制

在介绍了进程环境之后,这篇文章将进一步介绍进程的控制,包括创建新进程、执行程序和进程终止。

进程标识符
每个进程都有一个非负整型表示的唯一进程 ID,称为进程标识符。虽然进程ID是唯一的,但是进程 ID 可以被复用,当一个进程终止时,其进程 ID 就可以用于另一个新的进程。通过采用延迟重用算法,使得赋予新进程的 ID 不同于最近终止的进程所使用的 ID,防止将新进程误认为是使用同一个 ID的已经终止的前进程。

  • 系统中专用进程
    1、swapper进程
    交换进程是 UNIX 系统的调度进程,其进程 ID 为 0.该进程是内核的一部分,它并不执行任何磁盘上的程序。
    2、Init进程
    init进程的进程ID为1,在自举过程结束时由内核调用。它通常读取与系统有关的初始化文件,并将系统引导到一个状态。init进程绝不会终止,它不同于swapper进程,它不是一个内核进程,是一个用户进程,但是它以超级用户特权运行,此外,init进程是所有孤儿进程的父进程。
    3、页守护进程
    进程ID为2,此进程支持负责支持虚拟存储系统的分页操作。

  • 获取进程ID的函数

#include <unistd.h>
pid_t getpid(void);
返回值:调用进程的进程ID
pid_t getppid(void);
返回值:调用进程的父进程ID
uid_t getuid(void); //调用进程的实际用户ID
uid_t geteuid(void);//调用进程的有效用户ID
gid_t getgid(void);//调用进程的实际组ID
gid_t getgid(void);//调用进程的有效组ID

进程创建fork和vfork

  • fork函数
#include <unistd.h>
pid_t fork(void)
返回值:子进程中返回0,父进程中返回子进程ID,出错则返回-1

由 fork 创建的新进程被称为子进程。fork 函数调用一次,但返回两次。两次返回的唯一区别是:子进程返回值为 0,而父进程的返回值是新子进程的进程 ID。为什么要将子进程ID返回给父进程?因为:一个进程的子进程可以有多个,然而没有提供一个函数以获取某个进程的所有子进程 ID,所以父进程可以通过 fork 函数的返回值获取其子进程的进程 ID,并进行后续的处理。而相反,一个进程只有一个父进程,在子进程中,可以通过调用 getppid 函数获取其父进程的进程 ID(进程ID为0是swapper进程,所以一个子进程的ID不可能为0)。
fork 函数返回之后,子进程和父进程都各自继续执行 fork 调用之后的指令。子进程是父进程的副本。例如,子进程获得了父进程数据空间、堆和栈的副本。意味着父子进程之间不共享这些存储空间,他们之间共享的存储空间只有程序正文段。(说明:这里通常采用写时复制,这些区域由父进程和子进程共享,当父进程和子进程中的任何一个试图修改这些区域,内核只会为修改的那部分区域制作一个副本,通常是虚拟存储系统上的一页。)
例子:

#include "apue.h"

int     globvar = 6;        /* external variable in initialized data */
char    buf[] = "a write to stdout\n";

int
main(void)
{
    int     var;        /* automatic variable on the stack */
    pid_t   pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
        err_sys("write error");
    printf("before fork\n");    /* we don't flush stdout */

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {      /* child */
        globvar++;              /* modify variables */
        var++;
    } else {
        sleep(2);               /* parent */
    }

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
      var);
    exit(0);
}

对应的运行结果如下:

xucheng@xucheng-Inspiron-5520:~/xc/test g c c f o r k . c o f o r k x u c h e n g @ x u c h e n g I n s p i r o n 5520 :   / x c / t e s t ./fork
a write to stdout
before fork
pid = 6536, glob = 7, var = 89
pid = 6535, glob = 6, var = 88

由上述结果可以看到子进程的结果改变了。一般来说,在fork之后是父进程先执行还是父进程执行,是不确定的,这取决于内核的调度算法,在这里通过延迟2妙来让子进程先执行。
对于上面实例中,由于write函数是无缓冲的,所以在创建子进程之前就被输出到标准设备上,但是标准I/O库是带缓冲的,当其被绑定到终端设备上时是行缓冲的,否则他是全缓冲的。例如我们在这里将标准输出重定向到一个文件,可以看到如下结果:

xucheng@xucheng-Inspiron-5520:~/xc/test . / f o r k > c c x u c h e n g @ x u c h e n g I n s p i r o n 5520 :   / x c / t e s t cat cc
a write to stdout
before fork
pid = 6665, glob = 7, var = 89
before fork
pid = 6663, glob = 6, var = 88

这里可以看到printf输出了行两次,因为,在fork之前调用了printf一次,当调用fork时,数据仍在缓冲区,然后将父进程数据空间复制到子进程空间时,该缓冲区也被复制到子进程缓冲区。
对于上面还要说明的一个问题是:在重定向父进程的标准输出时,子进程的标准输出也被重定向。fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中,父子进程每一个相同的打开文件描述符共享同一个文件表项:
这里写图片描述
可以看到这里父进程子进程共享同一个文件偏移量,如果父进程和子进程写同一文件描述符指定的文件,但有没有任何形式的同步,输出就会相互混合,在我们的例子中,是让子进程执行完,此时文件偏移已经更新,再执行父进程。

  • vfork函数
pid_t vfork(void);

vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec,于是也就不会引用该地址空间。不过子进程在调用exec或exit之前,它在父进程的空间中进行。此外vfork 保证子进程先运行,在调用exec或exit之后父进程才可以被调度运行。如果在调用exec和exit之前子进程依赖于父进程的进一步动作会造成死锁
例子:

#include "apue.h"

int     globvar = 6;        /* external variable in initialized data */

int
main(void)
{
    int     var;        /* automatic variable on the stack */
    pid_t   pid;

    var = 88;
    printf("before vfork\n");   /* we don't flush stdio */
    if ((pid = vfork()) < 0) {
        err_sys("vfork error");
    } else if (pid == 0) {      /* child */
        globvar++;              /* modify parent's variables */
        var++;
        _exit(0);               /* child terminates */
    }

    /* parent continues here */
    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
      var);
    exit(0);
}

此时运行的结果为:

xucheng@xucheng-Inspiron-5520:~/xc/test$ ./vfork
before vfork
pid = 6863, glob = 7, var = 89

可以看到子进程对变量做增1的操作结果影响了父进程中的值,因为在exec或exit之前在父进程的地址空间中进行。

进程终止
进程环境中讲到了进程的终止方式。为了方便说明这里在此列出:


  1. 从 main 返回。
  2. 调用 exit。
  3. 调用_exit 或_Exit。
  4. 最后一个线程从其启动例程返回。
  5. 最后一个线程调用pthread_exit。
    另外三种为异常终止方式,它们是
  6. 调用 abort。
  7. 接到一个信号并终止。
  8. 最后一个线程对取消请求做出响应。

对上述进程的终止,不管是哪一种情况,最终都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开文件描述符,释放他所使用的寄存器。我们都希望终止进程能够通知其父进程它是如何终止的。对于三个exit 函数的参数作为程序的退出状态(exit status)。在进程终止时,内核会将程序的退出状态作为进程的终止状态(termination status)。在进程异常终止的时候,内核会产生一个指示其异常终止原因的终止状态。终止进程的父进程都能用wait或waitpid函数取得其终止状态。对于两种请款需要特殊说明:
1、如果父进程在子进程终止之前终止,对于父进程已经终止的所有子进程,他们的父进程都改为 init进程(pid为1) 我们称这些子进程 由init进程领养。操作过程大致是,在一个进程终止时,内核逐个检查所有活动进程,判断他是否是正要终止进程的子进程,如果是,则该进程的父进程ID就为1。
2、如果子进程在父进程之前终止,内核为每个终止的子进程保存了一定量的信息,所以当终止进程的父进程 调用wait 和 waitpid 时,可以得到这些信息(包含了进程ID、该进程的终止状态、以及该进程使用的CPU时间总量等)。
特别说明:对于一个已经终止、但是父进程尚未对其进行善后处理(获取终止子进程的有关信息并释放它占用的资源) 的进程被称为僵尸进程。
那么如果那些被 init 进程领养的进程在终止之后会不会也变成僵尸进程?答案是:不会。因为 init 进程无论何时只要有一个子进程终止,init 就会调用 wait 函数获取其终止状态。

  • wait函数
#include <sys/wait.h>
pid_t wait(int *statloc);
返回值:若成功则返回进程ID,若出错则返回-1
• 如果其所有子进程都还在运行,则阻塞。
• 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态然后立即返回。
• 如果没有任何子进程,则立即出错返回。
• statloc是一个整形指针,如果不是一个空指针,则终止进程的状态就放在他所指向的单元。

其终止状态用定义在

#include "apue.h"
#include <sys/wait.h>

void
pr_exit(int status)
{
    if (WIFEXITED(status))
        printf("normal termination, exit status = %d\n",
                WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        printf("abnormal termination, signal number = %d%s\n",
                WTERMSIG(status));
    else if (WIFSTOPPED(status))
        printf("child stopped, signal number = %d\n",
                WSTOPSIG(status));
}

int
main(void)
{
    pid_t   pid;
    int     status;

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)              /* child */
        exit(7);

    if (wait(&status) != pid)       /* wait for child */
        err_sys("wait error");
    pr_exit(status);                /* and print its status */

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)              /* child */
        abort();                    /* generates SIGABRT */

    if (wait(&status) != pid)       /* wait for child */
        err_sys("wait error");
    pr_exit(status);                /* and print its status */



    exit(0);
}

运行结果如下:

xucheng@xucheng-Inspiron-5520:~/xc/test$ ./wait
normal termination, exit status = 7
abnormal termination, signal number = 6(null)

  • waitpid函数
    如果一个进程有几个子进程,那么只要有一个子进程终止,wait就返回。如果要等待一个特定的进程终止,我们就应该使用waitpid函数。
    相比于wait主要有两点区别:
    1、在一个子进程终止前,wait使其调用者阻塞,waitpid有一选项,可以是调用者不阻塞。
    2、waitpid并不等待在其调用之后的第一个终止子进程,它有很多选项,可以控制它等待的进程。
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)

waitpid多出了两个可由用户控制的参数pid和options.,下面介绍这两个参数:
从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。   
 1、pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。    
 2、pid= -1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。 
 3、pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。    
 4、pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
 
option参数提供了一些额外的选项来控制waitpid,option 可以为 0 或可以用”|”运算符把它们连接起来使用。。
1、WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
2、 WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。WIFSTOPPED(status)宏确定返回值是否对应与一个暂停子进程。
例如:在几乎同一时刻,有N个client 的 FIN发向服务器,同样的,N个SIGCHLD信号到达服务器,然而,UNIX的信号往往是不会排队的,显然这样一来,信号处理函数将只会执行一次,残留剩余N-1个子进程作为僵尸进程驻留在内核空间。此时,正确的解决办法是利用waitpid(-1, &stat, WNOHANG)防止留下僵尸进程。其中的pid为-1表明等待任一个终止的子进程,而WNOHANG选择项通知内核在没有已终止进程项时不要阻塞。
例如:

#include"apue.h"
#include <sys/wait.h>

int
main(void)
{
  pid_t pid1, pid2;
  pid_t waitpidRes;

  if ((pid1 = fork()) < 0) {
    err_sys("fork error");
  } else if (pid1 == 0) {
    sleep(3);
    printf("child1 process %d exit\n", getpid());
    exit(0);
  }

  if ((pid2 = fork()) < 0) {
    err_sys("fork error");
  } else if (pid2 == 0) {
    printf("child2 process %d exit\n", getpid());
    exit(0);
  }

  if ((waitpidRes = waitpid(pid1, NULL, 0)) == pid1) {
    printf("get terminated child process %d.\n", waitpidRes);
  } else if (waitpidRes < 0) {
    err_sys("waitpid error");
  } else {
    printf("waitpid return 0\n");
  }
  printf("parent process exit\n");

  exit(0);
}

运行结果如下:

xucheng@xucheng-Inspiron-5520:~/xc/test$ gcc waitpid.c -o waitpid
xucheng@xucheng-Inspiron-5520:~/xc/test$ ./waitpid 
child2 process 7861 exit
child1 process 7860 exit
get terminated child process 7860.
parent process exit

可以看到父进程阻塞等待子进程7860终止,当把可选参数改为WNOHANG时运行结果如下:

xucheng@xucheng-Inspiron-5520:~/xc/test$ gcc waitpid.c -o waitpid
xucheng@xucheng-Inspiron-5520:~/xc/test$ ./waitpid 
waitpid return 0
parent process exit
child2 process 7903 exit
xucheng@xucheng-Inspiron-5520:~/xc/test$ child1 process 7902 exit

此时指定了参数WNOHANG,如果此时指定的进程不是立即可用的,waitpid函数返回0,不会阻塞等待。

举例说明:一个进程fork一个子进程,但不要他等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,此时可以fork两次来避免产生僵尸进程。

#include "apue.h"
#include <sys/wait.h>

int
main(void)
{
    pid_t   pid;

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {      /* first child */
        if ((pid = fork()) < 0)
            err_sys("fork error");
        else if (pid > 0){
            printf("first child process: %d, parent process: %d\n", getpid(),getppid());
            exit(0);    /* parent from second fork == first child */
        }
        /*
         * We're the second child; our parent becomes init as soon
         * as our real parent calls exit() in the statement above.
         * Here's where we'd continue executing, knowing that when
         * we're done, init will reap our status.
         */
        sleep(2);
        printf("second child process: %d, parent process: %d\n", getpid(), getppid());
        exit(0);

    }
    if (waitpid(pid, NULL, 0) != pid)   /* wait for first child */
        err_sys("waitpid error");

    /*
     * We're the parent (the original process); we continue executing,
     * knowing that we're not the parent of the second child.
     */
    printf("parent process %d exit\n", getpid());
    exit(0);
}

结果如下:

> xucheng@xucheng-Inspiron-5520:~/xc/test$ gcc forkfork.c -o forkfork
> xucheng@xucheng-Inspiron-5520:~/xc/test$ ./forkfork 
first child process: 8343, parent process: 8342
parent process 8342 exit
xucheng@xucheng-Inspiron-5520:~/xc/test$ second child process: 8344, parent process: 1

当第一个子进程8343终止之后,其子进程8344的父进程ID变成了1(init进程)。NOTE:当原先的进程(也就是exec本程序的进程)终止时,shell打印其提示符。

竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,此时认为发生了竞争条件。fork函数会使竞争条件的滋生地,通常我们不能预料哪一个进程先运行,这依赖于系统的负载以及内核的调度算法。
根据上面的wait函数可以知道,如果一个进程希望等待其子进程终止,他必须调用wait函数中的一个。如果一个进程要等待其父进程终止,可以使用下面方式的循环:

while(getppid()!=1)
    sleep(1);

这种形式的循环称为轮询,但是它浪费了CPU时间,因为调用者每隔一秒就会被唤醒,进行条件测试。为了避免竞争条件,多个进程之间有某种形式的信号发送和接收方法。
下面看一个竞争条件的实例:

#include "apue.h"

static void charatatime(char *);

int
main(void)
{
    pid_t   pid;

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        charatatime("output from child\n");
    } else {
        charatatime("output from parent\n");
    }
    exit(0);
}

static void
charatatime(char *str)
{
    char    *ptr;
    int     c;

    setbuf(stdout, NULL);           /* 设置为不带缓冲的,每个字符输出一次 */
    for (ptr = str; (c = *ptr++) != 0; )
        putc(c, stdout);
}

运行结果如下:

xucheng@xucheng-Inspiron-5520:~/xc/test$ ./a.out 
output from parento
utput from child
xucheng@xucheng-Inspiron-5520:~/xc/test$ ./a.out 
output from parent
output from child
xucheng@xucheng-Inspiron-5520:~/xc/test$ ./a.out 
output from parento
utput from child

把标准输出设置为不带缓冲的,所以每个字符都会输出一遍。此时,由于发生了竞争条件,所以会出现上述结果。

猜你喜欢

转载自blog.csdn.net/xc13212777631/article/details/81016271