进程与信号学习

一、进程结构  

  UNIX标准把进程定义为:“一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。”正在运行的程序或进程由程序代码、数据、变量(占用着系统内存)、打开的文件(文件描述符)和环境组成。一般来说,Linux系统会在进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有代码的一份副本。

  每个进程都会被分配一个唯一的数学编号,我们称之为进程标识符或PID。它通常是一个取值范围从2到32768的正整数。数字1一般是特殊进程init保留的,init进程负责管理其它进程。

进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用与返回,进程还有自己的环境空间,包含专门为这个进程建立的环境变量;进程还必须维护自己的程序计数器,这个计数器用来记录它执行到的位置,即在执行线程中的位置。因为Linux和Unix一样,有一个虚拟内存系统,能够把程序代码和数据以内存页面的形式放到硬盘的一个区域中,所以Linux可以管理的进程比物理内存所能容纳的要多得多。

二、启动新进程

  在一个程序的内部启动另一个程序,从而创建一个新进程,这个工作可以通过库函数system来完成:

#include<stdio.h>
int system(const char *string);
system("ps ax &");        //system使用例子(& 表示后台运行)

  system函数的作用是,运行以字符串参数的形式传递给它的命令并等待该命令的完成。命令的执行情况就如同在shell中执行如下的命令:

$ sh -c string

1、替换进程映像

  exec系列函数由一组相关的函数组成,它们在进程的启动方式和程序参数的表达方式上各有不同,exec函数可以把当前进程替换为一个新进程,新进程由path或file参数指定。

#include<unistd.h>
char **environ;
int
execl(const char *path, const char *arg, ...,(char *)0); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg, ...,(char *)0,char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...,(char *)0);
int execvp(const char *file, char *const argv[]);

   这些函数可以分为两大类。execl、execlp和execle的参数个数是可变,参数以一个空指针结束。execv和execvp的第二个参数是一个字符串数组。不管是哪种情况,新程序在启动时会把在argv数组中给定的参数传递给main函数。

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

int main()
{
    printf("Running ps with execlp\n");
    execlp("ps", "ps", "ax", 0);
    printf("Done.\n");
    exit(0);
}

2、复制进程映像

  我们可以通过调用fork创建一个新进程。这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的,新进程几乎与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、环境和文件描述符。fork和exec函数结合在一起使用就是创建新进程所需要的一切了。一个典型的使用fork调用片段如下所示:

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

pid_t new_pid; new_pid
=fork(); switch(new_pid){ case -1: //Error   break; case 0: //we are child   break; default : //we are parent   break; }

3、等待一个进程

  当用fork启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。我们可以通过在父进程中调用wait函数让父进程等待子进程的结束。

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int *stat_loc);

  wait系统调用将暂停父进程直到它的子进程结束为止,这个调用返回子进程的PID,它通常是已经结束运行的子进程的PID。

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

int main()
{
    pid_t pid;
    char *message;
    int n;
    int exit_code;

    printf("fork program starting\n");
    pid = fork();
    switch(pid) 
    {
    case -1:
        exit(1);
    case 0:
        message = "This is the child";
        n = 5;
        exit_code = 37;
        break;
    default:
        message = "This is the parent";
        n = 3;
        exit_code = 0;
        break;
    }

    for(; n > 0; n--) {
        puts(message);
        sleep(1);
    }

/*  This section of the program waits for the child process to finish.  */

    if(pid) {
        int stat_val;
        pid_t child_pid;

        child_pid = wait(&stat_val);

        printf("Child has finished: PID = %d\n", child_pid);
        if(WIFEXITED(stat_val))
            printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
        else
            printf("Child terminated abnormally\n");
    }
    exit (exit_code);
}

4、僵尸进程

  子进程终止时,它与父进程之间的关联还会保持,知道父进程也正常终止或父进程调用wait才告结束,因此,进程表中代表子进程的表项不会立刻释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时它将成为一个死(defunct)进程或僵尸(zombie)进程。

  还有另一个系统调用可用来等待子进程的结束,它是waitpid函数。你可以用它来等待某个特定进程的结束。

#include<sys/types.h>
#include<sys/wait.h>

pid_t waitpid(pid_t pid,int *stat_loc,int options);

三、信号

  1、信号是UNIX和Linux系统响应某些条件而产生的一个事件。接收到该信号的进程会相应地采取一些行动。我们用术语生成(raise)表示一个信号的产生,使用术语捕获(catch)表示接收到一个信号。如果进程接收到这些信号中的一个,但事先没有安排捕获它,进程将会立刻终止。程序可以用signal库函数来处理信号,它的定义如下:

#include<signal.h> 

void (*signal(int sig, void (*func(int)))(int);

  这个相当复杂的函数定义说明,signal是一个带有sig和func两个参数的函数。准备捕获或忽略的信号有参数sig给出,接收到指定的信号后将要调用的函数由参数func给出。信号处理函数必须有一个int类型的参数(即接收到的信号代码)并且返回类型为void。signal函数本身也返回一个同类型的函数,即先前用来处理这个信号的函数,或者也可以用以下的两个特殊值之一来代替信号处理函数。

SIG_IGN 忽略信号
SIG_DFL 恢复默认行为
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void ouch(int sig)
{
    printf("OUCH! - I got signal %d\n", sig);
    (void) signal(SIGINT, SIG_DFL);
}
int main()
{
    (void) signal(SIGINT, ouch);
    while(1) {
        printf("Hello World!\n");
        sleep(1);
    }
}

2、发送信号

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);

  kill函数把参数sig给定的信号发送给由参数pid给出的进程号所指定的进程,成功时它返回0。要想发送一个信号,发送进程必须拥有相应的权限,这通常意味两个进程必须拥有相同的用户ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。

  信号向我们提供了一个有用的闹钟功能,进程可以通过调用alarm函数在经过预定时间后发送一个SIGALRM(超时警告)信号。

#include<unistd.h>
unsigned int alarm(unsigned int seconds);

  alarm函数用来在seconds秒之后安排发送一个SIGALRM信号。

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

static int alarm_fired = 0;

void ding(int sig)
{
    alarm_fired = 1;
}
int main()
{
    pid_t pid;
    printf("alarm application starting\n");
    pid = fork();
    switch(pid) {
    case -1:
      /* Failure */
      perror("fork failed");
      exit(1);
    case 0:
      /* child */
        sleep(5);
        kill(getppid(), SIGALRM);
        exit(0);
    }
    printf("waiting for alarm to go off\n");
    (void) signal(SIGALRM, ding);

    pause();
    if (alarm_fired)
        printf("Ding!\n");

    printf("done\n");
    exit(0);
}

  在main函数中,我们告诉子进程在等待5秒后发送一个SIGALRM(超时警告)信号给它的父进程,父进程通过一个signal调用安排好捕获SIGALRM信号的工作,然后等待它的到来。运行这个程序,它会暂停5s,等待模拟闹钟的闹醒。这个程序用到了pause函数,它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止。

posted @ 2017-09-25 21:20  一缕残雪  阅读(288)  评论(0编辑  收藏  举报