3 进程 的简单学习

参考文献:Advanced.Linux.Programming ,可以从我的上传文件中免费下载。

                         Linux 编程手册,部分例子是我自己写的。

1. 什么是进程?

   简单回答,一个程序的运行实例称为一个进程。

2. 进程的IDs

    在Linux系统中每一个进程有一个唯一的进程ID 标识,或称作 pid。他是一个16-bit的数字。除了init进程外每个进程都有父进程,称为ppid。你可以使用 $ ps -l 来观察当前系统中的进程。

     在C 或者C++中 我们可以利用 getpid() 这个系统调用得到当前进程的进程ID。可以利用geppid()系统调用得到当前进程的父进程ID。当然,你要包含头文件 <sys/types.h>。下面是个小例子。

 //        ( print-pid.c) Printing the Process ID
#include <stdio.h>
#include <unistd.h>

#include <sys/types.h>
int main ()
{
  printf (“The process ID is %d/n”, (int) getpid ());
  printf (“The parent process ID is %d/n”, (int) getppid ());
  return 0;
}
//使用 gcc -o print-pid print-pid.c 编译

3. 杀死一个进程。

   你可以使用 $kill pid_of_the_process 杀死一个正在运行的进程。实际上,kill命令通过向 目标进程发送 SIGTERM信号起作用的。但是,如果改进程屏蔽了SIGTERM信号那就不会起作用。

4.创建一个进程。

   有两种方式创建一个进程。

    4.1 使用 system()系统调用。这种方式是不提倡的。

          例子如下,

//(system.c) Using the system Call
#include <stdlib.h>
int main ()
{
  int return_value;
  return_value = system (“ls -l /”);
  return return_value;
}

4.2 使用 fork 和 exec

       ** 使用fork()

           当在一个进程中调用 fork()时,就会产生一个当前进程的复制版,这个复制版被称为 子进程。当前进程和子进程将继续从调用fork()的位置往下执行。这里有个问题,怎样区分当前进程和新建的子进程? 回答是,子进程有独立的 进程ID,另外,fork() 的返回值在父进程和子进程中是不同。在新建立的子进程中返回值为0 ,在父进程中返回值为新进程的进程ID。下面是个小例子。

 

  //sting 3.3 ( fork.c) Using fork to Duplicate a Program’s Process
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main ()
{
  pid_t child_pid;
  printf("the main program process ID is %d/n", (int) getpid ());
  child_pid = fork ();
  if (child_pid != 0) {
     printf ("this is the parent process, with id %d/n", (int) getpid ());
     printf ("the child’s process ID is %d/n", (int) child_pid);
     getchar();
  }
  else{
     printf ("this is the child process, with id %d/n", (int) getpid ());
     getchar();
 }
 return 0;
}
注意:在上面的例子里,我们故意加上了getchar()让父子进程都处于等待输入的状态。你可以使用$ ps -al 来查看父进程和子进程。注意观察 pid ,ppid和CMD三栏。(同时按住ctr+shift+t 可以在同一个窗口打开一个新的终端)

 

**使用exec函数族

   1> execvp(执行文件)
  相关函数 fork,execl,execle,execlp,execv,execve
  表头文件 #include<unistd.h>
  定义函数 int execvp(const char *file ,char * const argv []);
  函数说明 execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行 的文件。返回值 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。例子,

 #include<unistd.h>
  main()
  {
   char * argv[ ] ={ “ls”,”-al”,”/etc/passwd”,0};
   execvp(“ls”,argv);
  }

 2>execve(执行文件)
相关函数 fork,execl,execle,execlp,execv,execvp
表头文件 #include<unistd.h>
定义函数 int execve(const char * filename,char * const argv[ ],char
* const envp[ ]);
函数说明 execve()用来执行参数filename字符串所代表的文件路径,第二个
参数系利用数组指针来传递给执行文件,最后一个参数则为传递给
执行文件的新环境变量数组。
返回值 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因
存于errno 中。

#include<unistd.h>
main()
{
char * argv[ ]={“ls”,”-al”,”/etc/passwd”,(char *)0};
char * envp[ ]={“PATH=/bin”,0}
execve(“/bin/ls”,argv,envp);
}

3>execl,execlp,execv类似请自己查找有关使用手册。

 

 

 

***联合使用 fork 和 exec

//suadd.c

#include <stdio.h>

int main(int argc,char** argv)
{
        int a,b;
        a = atoi(argv[1]);
        b = atoi(argv[2]);
        while(1){
                printf("The subadd result is %d/n",a+b);
                sleep(2);
        }
        return 0;
}

//编译:$ gcc -o subadd subadd.c

//exec_fork.c

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

int main(void)
{
        pid_t child_pid;
        char* envp[] = {"PATH=./",'/0'};
        char* argv[] = {"subadd","1","2",'/0'};
        child_pid = fork();//create a new process
        execve("subadd",argv,envp);
        while(1){
                printf("This is main process.ID is %d/n",getpid());
        //      sleep(2);
        }
}

//编译:$ gcc -o exec_fork exec_fork.c
//将上面的两个例子编译完成后,执行$ ./exec_fork  结果为:

The subadd result is 3

The subadd result is 3

........(省略)

实际上,execve调用永远不会返回除非 执行 subadd时发生了错误。这样,父进程停止执行,而开始执行一个新的进程。

 

 

3.2.3  进程的调度 (往下我就开始使用原书中的标号以方便你查阅,上面的不想改了,见谅)

        父子进程进行独立的调度。没有谁能保证父进程被先执行或者子进程被先执行。

要想改变进程的优先级可以改变进程的niceness. 这个数值越大优先级越低,反之越高。你可以选择负数来获得高的优先级。改变niceness可以使用 $ nice -n 10 yourprogram      也可以使用renice 修改正在运行的进程的优先级。但是,进行相应测试之前你应该拥有root权限。在程序中使用nice()来改变当前进程的优先级。

为了验证,我们使用如下的例子,

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

int main(void)
{
        pid_t sub_id;
        unsigned int counter = 0;

        sub_id = fork();
        if(sub_id == 0){//child process
//              nice(-2);//change sub process's privilege
                while(counter<0xffffffff){
                        counter++;
                }
                printf("SUB is end/n");
        }else{
                nice(-2);//change Parent process's privilege
                while(counter<0xffffffff){
                        counter++;
                }
                printf("Parent is end/n");
        }
}

//说明,你可以通过上边的例子 中 字符串 打印的顺序看出谁的优先级更高。

 

3.3  信号(Signals)

  在Linux中信号是进程间通信和控制进程的体制。信号是发送给进程的特殊消息。它的发送是异步的。而一个进程一旦收到某个信号就会立即做出相应的处理,不管现在执行的是什么操作。信号有很多种,它的类型由它的信号数值决定,而在程序中你可以使用它的名字来使用它。这数值与名字的对应在/usr/include/bits/signum.h中有定义(注意:在你的程序中你不可以直接使用该头文件,而要使用<signal.h>)。

    当有进程收到信号时会依据对信号处理函数(disposition)的设定执行相应的处理。如果你没有设定信号的disposition,则默认的信号处理函数(disposition)将会执行。在执行disposition处理时原来的进程会暂停当前的工作。直到disposition执行完毕后,才返回进程继续执行,或者进程终止。

    一个进程可以向另一个进程发送信号。SIGUSR1,SIGUSR2 是两个为用户保留的信号。用户可以依据自己的需要设置这两个信号的disposition处理。从而让进程在收到SIGUSR1,SIGUSR2信号时执行预定的动作。

    sigaction()被用来设定信号的处理函数(disposition)。

表头文件 #include<signal.h>
定义函数 int sigaction(int signum,const struct sigaction
*act ,struct sigaction *oldact);
函数说明 sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
如参数结构sigaction定义如下
struct sigaction
{
void (*sa_handler) (int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}
sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal()。
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号搁
置。
sa_restorer 此参数没有使用。
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
OR 运算(|)组合A_NOCLDSTOP : 如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式。
SA_RESTART:被信号中断的系统调用会自行重启
SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次
到来。如果参数oldact不是NULL指针,则原来的信号处理方式会由此结构sigaction 返回。
返回值 执行成功则返回0,如果有错误则返回-1。
错误代码 EINVAL 参数signum 不合法, 或是企图拦截
SIGKILL/SIGSTOPSIGKILL信号
EFAULT 参数act,oldact指针地址无法存取。
EINTR 此调用被中断

 

       因为信号是异步的,当信号处理函数正在执行时,主程序或许处于一种非常不稳定,脆弱的(fragile)状态。所以,你不应当在信号处理函数中执行任何的I/O操作或者也不能调用大部分的库和系统函数。

      信号处理函数应当执行最少的操作来相应信号,然后把控制权交还给主程序(或者 终止程序)。大部分情况下,信号处理函数中仅仅记录下该信号发生了。而在主程序中循环的检测是否有信号产生,并做出相应的处理。

       一个信号的处理函数执行过程可能被另外一个信号终止。虽然这听起来很少发生,但是一旦发生了将会非常难以诊断和调试。因此,你应当非常谨慎地处理你的信号处理函数。

      在信号处理函数中,即使简单的给一个全局变量赋值也是危险的,因为这个赋值过程可能会被分解为两个或者更多的机器指令,而当第二个信号在这个过程中发生了,那么这个变量的值将会处于一种不正确的状态。如果你想要使用一个全局变量来标识一个信号的发生,那么你应当使用一种特殊的数据类型--- sig_atomic_t。 Linux将保证对这种变量的赋值在单个指令中完成,而不会在中途被打断。在Linux中 sig_atomic_t是一般的int类型。事实上,对整数类型的如同int大小的或者更小的,或者指针类型的赋值都是元子操作。如果你想要使你写的程序可以移植到任何标准UNIX系统,那么你应当使用 sig_atomic_t类型作为全局变量。

 

    如下是个小例子,这个例子演示的 SIGUSR1信号处理函数的设定,以及主程序和信号处理函数的执行顺序。

#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

sig_atomic_t sigusr1_count = 0;

void handler(int signal_number)
{
        sigusr1_count++;
        printf("One signal received/n");
        sleep(1);
}

int main(void)
{
        struct sigaction sa;
        unsigned int delay = 0;
        bzero((void*)&sa,sizeof(sa));
        sa.sa_handler = &handler;
        sigaction(SIGUSR1, &sa, NULL);
        for(delay = 0;delay < 0xffffffff;){//No sence, Just for delay
                delay = delay-1;
                delay++;
                delay++;
              //  printf("*");//先别屏蔽这一行,执行一次,再屏蔽此行一次。
        }
        printf("SIGUSR1 was raised %d times/n", sigusr1_count);

        return 0;
}

 

注意:你应当打开两个终端,在其中一个中执行本程序,在另外一个中 先执行 $ps -al 查看本进程的进程ID,再执行 $kill -s SIGUSR1 your_pid (多执行几次)看下 主程序的反应。

 

 

3.4  进程的终结

 

        通常在如下的两种情况下进程将停止。一种是,正在执行的进程调用了 exit 函数。另一种是,程序的 main 函数返回。每个进程都有一个退出码。这个退出码是个数,它将返回给它的父进程。退出码就是 exit 函数的参数,或者 return 后面跟着的那个数。

        一个进程或许会响应相应的信号而异常地终止。比如,SIGBUS,SIGSEGV,SIGFPE。当用户尝试着在终端使用Ctrl+C结束一个进程时,SIGINT信号将被发送给那个正在执行的进程。SIGTERM 信号是被 kill 命令发出的。对这两个信号的默认的处理都是 结束进程。通过调用abort 函数,一个进程向自己发送SIGABRT信号,这将终止这个进程并且产生一个核心文件(core file)。最强大的是SIGKILL信号,它将立即终止一个进程,这个信号不会被挂起或者被信号处理函数处理。在终端上使用 kill来发送这些信号,在上面的例子中有说明。在程序中发送一个信号要使用 kill 这个函数。 比如,

                                                            kill(child_pid, SIGTERM);//包含<sys/types.h>和<signal.h>

        通常,我们用退出码指示是否程序正常退出了。退出码为0时表示程序正常的退出了。否则不是。后来,我们返回不同的值来指示错误的性质。这始终好的传统,在其他GNU/Linux组件中也认可这种行为。这样你可以在main函数中返回0除非错误发生了。

      最近执行的程序的退出码保存在变量$?中。你可以 $echo $? 来观察这个值。

 

 

 

 

3.4.1  等待进程的终止

 

     在某些情况下,我们需要让父进程等待,直到所有的子进程终止。这时你就需要使用wait 函数族。

 

3.4.2  系统调用 wait

           最简单的情况是使用 wait。它阻塞调用它的进程直到该进程的一个退出(或者发生了错误)。它通过一个整型的指正参数返回一个状态码。比如,WEXITSTATUS 宏提取出子进程的退出码。

         你可以使用WIFEXITED 宏来检测一个子进程的退出状态,看看它是正常的退出还是被信号杀死。对于后一种情况,你可以使用WTERMSIG 宏来提取出导致该进程死亡的信号。

      下面的例子,在主进程中调用wait 来等待子进程的退出。

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int spawn(char* program, char** arg_list)
{
        pid_t child_pid;
        child_pid = fork();
        if(child_pid!=0){//This is the parent process
                return child_pid;
        }else{
                execvp(program, arg_list);
                fprintf(stderr,"error occurred/n");//this can't be excuted unless execvp failed
        }
}
int main(void)
{
        int child_status;
        //The argument list to pass to the "ls" command.
        char* arg_list[] = {
                "find",//The name of the program
                "/",
                "nothisfile",
                NULL //The list must end with a NULL
        };
        //Spawn a child process running the "ls" command.
        spawn("find", arg_list);
        //Wait for the child process to complete.
        wait(&child_status);
        if(WIFEXITED (child_status)){
                printf("The child process exited normally,with exit code %d /n",WEXITSTATUS(child_status));
        }else{
         printf("The child process was killed by the signal %d",WTERMSIG(child_status));
}

        return 0;
}

 

//  你可以在一个终端中执行该程序,在另外一个终端中执行 $ps -al 查到 当前进程 pid 再执行 $ kill  pid_of_your_precess 观察程序的反应。

//注: 15 是信号 TERM

 

在Linux 中有几个类似的系统调用,它们比wait更复杂,功能也更多。如 waitpid 可以等待指定的进程退出而不是任意的某一进程的退出。wait3返回退出进程的CPU利用的统计情况。wait4允许你指定附加选项。请自己查阅相应函数的使用方法。

 

 

3.4.3 僵尸进程

      僵尸进程是一个进程已经停止但是还没有被清理的状态。清理一个已经结束了的进程的资源属于它的父进程的责任。而父进程的wait函数就可以进程这种哦功能操作。当子进程在父进程调用 wait函数前结束的话,那么子进程将成为僵尸进程。

      一个僵尸进程不会一直在系统中存在因为他将被init进程收留,并且自动清理掉。

     如下,是个僵尸进程的例子

 #include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
        pid_t child_pid;
        //Create a child process
        child_pid = fork();
        if(child_pid > 0){
        //This is the parent process
         sleep(60);
        }else{
                //This the child process
                exit(0);
        }
        return 0;
}

 

开始执行以上的程序后,你打开另外一个终端,$ ps -e -o pid,ppid,stat,cmd

你会在最下面发现   Z+   [xxxx] <defunct>   ,这里Z+ 表示 zombie 进程,即僵尸进程。过了一分钟后再次查看你会发现该僵尸进程消失了。

 

3.4.4 异步的清理子进程

 

     当一个子进程结束时他会向父进程发出SIGCHLD 信号,父进程可以在这个信号的处理函数中很优雅的完成子进程的清理工作。从而不产生僵尸进程。

#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
sig_atomic_t child_exit_status;
sig_atomic_t flag = 0 ;
void clean_up_child_process(int signal_number)
{
        //Clean up the child process
        int status;
        wait(&status);
        //Store its exit status in a global variable
        child_exit_status = status;
        flag = 1;
}
int main(void)
{
 //Handle SIGCHLD by calling clean_up_child_process
        pid_t child_pid;
        struct sigaction sigchld_action;
        memset(&sigchld_action,0,sizeof(sigchld_action));
        sigchld_action.sa_handler = &clean_up_child_process;
        sigaction(SIGCHLD,&sigchld_action,NULL);
        child_pid = fork();
        if(child_pid!=0){//This is the parent process
               while(!flag);
                printf("Main end/n");
        }else{
                printf("child process end /n");
        }
        return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值