Linux进程控制

来源:互联网 发布:土耳其软糖淘宝 编辑:程序博客网 时间:2024/06/10 02:15

                                    Linux进程控制

1.基本知识:

   (1)fork系统调用:创建一个新进程。即完成创建子进程,也返回一个值。

    <0,创建失败;=0,子进程执行中;〉0,主进程进行中。

   (2)getid:获得一个进程的id。

   (3)lockf:在进程同步控制中为进程加锁。

   (4)wait(等待子进程中断或结束):  进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait 就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

   (5)不像fork那么难理解,从exit的名字就能看出,这个系统调用是用来终止一个进程的。无论在程序中的什么位置,只要执行到exit系统调用,进程就会停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。


2.简单操作:

     (1)  编写一C语言程序(程序名为fork.c),使用系统调用fork( )创建两个子进程。当程序运行时,系统中有一个父进程和两个子进程在并发执行。父亲进程执行时屏幕显示“I am father”,儿子进程执行时屏幕显示“I am son”,女儿进程执行时屏幕显示“I am daughter”。

     (2)  多次连续反复运行这个程序,观察屏幕显示结果的顺序,直至出现不一样的情况为止。

     fork.c:

# include<stdio.h># include<sys/types.h># include<unistd.h>int main(){    int pid1,pid2    printf(“I am father!\n”);   if ((pid1 = fork())<0)   {      printf(“Child1 fail create!\n”);      return 1;   }   else if (pid1 == 0)   {      printf(“I am son!\n”);      return;   }   if ((pid2 = fork())<0)   {       printf(“Child2 fail create!\n”);       return 1;    }    else if (pid2 == 0)   {     printf(“I am daughter!\n”);     return;    }}

      (3)  编写一C语言程序(程序名为fork.c),使用系统调用fork( )创建一个子进程,然后在子进程中再创建子子进程。当程序运行时,系统中有一个父进程、一个子进程和一个子子进程在并发执行。父亲进程执行时屏幕显示“I am father”,儿子进程执行时屏幕显示“I am son”,孙子进程执行时屏幕显示“c”。

      (4)  多次连续反复运行这个程序,观察屏幕显示结果的顺序,直至出现不一样的情况为止。

      fork.c:

# include<stdio.h># include<sys/types.h># include<unistd.h>int main(){   int pid1,pid2   printf(“I am father!\n”);   if ((pid1 = fork())<0)   {      printf(“Child1 fail create!\n”);      return 1;   }   else if (pid1 == 0)   {  if ((pid2 = fork())<0)  {         printf(“c fail create!\n”);   return 1; }  else if (pid2 == 0) {   printf(“c\n”);  return;       }   else {  printf(“I am son!\n”);  return;  } }}

     (5) 修改程序,在父、子进程中分别使用wait、exit、lockf等系统调用“实现”其同步推进,多次反复运行改进后的程序,观察并记录运行结果。

     Linux进程控制wait()函数解析

     wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则          wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status 返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数status可以设成NULL。子进程的结束状态值请参考waitpid()。返回值如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。

     wait(等待子进程中断或结束),相关函数waitpid,fork。

     范例:

#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>int main(){  pid_t pid;  int status,i;  if(fork()= =0){    printf(“This is the child process .pid =%d\n”,getpid());    exit(5);  }  else{    sleep(1);    printf(“This is the parent process ,wait for child...\n”;    pid=wait(&status);    i=WEXITSTATUS(status);    printf(“child’s pid =%d .exit status=%d\n”,pid,i);  }}
     (6) Linux进程控制  exit()

     实例: 

/* exit_test1.c */#include<stdlib.h>main(){printf("this process will exit!\n");exit(0);printf("never be displayed!\n");}
      编译后运行: 

$gcc exit_test1.c -o exit_test1$./exit_test1this process will exit!
      我们可以看到,程序并没有打印后面的"never be displayed!\n",因为在此之前,在执行到exit(0)时,进程就已经终止了。

      exit 系统调用带有一个整数类型的参数status,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0表示没有意外的正常结束;其他的数值表示出现了错误,进程非正常结束。

     (7) Linux进程控制 lockf()

     利用系统调用lockf(fd,mode,size),对指定区域(有size指示)进行加锁或解锁,以实现进程的同步或互斥。其中,fd是文件描述字;mode是锁定方式,mode=1表示加锁,mode=0表示解锁;size是指定文件fd的指定区域,用0表示从当前位置到文件结尾(注:有些Linux系统是locking(fd,mode,size))。

    范例:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>int main(void){int pid1,pid2;lockf(1,1,0);printf(“Parent process:a\n”);if((pid1=fork())<0){printf(“child1 fail create\n”);return 1;}else if(pid1= =0){lockf(1,1,0);printf(“This is child1(pid=%d) process:b\n”,getpid());lockf(1,0,0);return;}if((pid2=fork())<0){printf(“child2 fail create\n”);return 1;}else if(pid2= =0){lockf(1,1,0);printf(“This is child2(pid=%d) process:c\n”,getpid());lockf(1,0,0);return;}}
     (8) wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status 返回,而子进程的进程识别码也会一快返回。参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,pid = wait(NULL)。返回值如果执行成功wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

     实例A:

/* wait1.c */#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <stdlib.h>main(){pid_t pc,pr;pc=fork();if(pc<0) /* 如果出错 */printf("error ocurred!\n");else if(pc==0){/* 如果是子进程 */ printf("This is child process with pid of %d\n",getpid());sleep(10);/* 睡眠10秒钟 */}else{/* 如果是父进程 */pr=wait(NULL);/* 在这里等待 */printf("I catched a child process with pid of %d\n"),pr);}exit(0);}

     编译并运行:

$ cc wait1.c -o wait1$ ./wait1This is child process with pid of 1608I catched a child process with pid of 1608

     可以明显注意到,在第2行结果打印出来前有10 秒钟的等待时间,这就是我们设定的让子进程睡眠的时间,只有子进程从睡眠中苏醒过来,它才能正常退出,也就才能被父进程捕捉到。其实这里我们不管设定子进程睡眠的时间有多长,父进程都会一直等待下去,读者如果有兴趣的话,可以试着自己修改一下这个数值,看看会出现怎样的结果。

     (9) 参数status

      如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:

     1)  WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

   (请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数--指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了)

     2)  WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。

    实例B:

/* wait2.c */#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>main(){int status;pid_t pc,pr;pc=fork();if(pc<0)/* 如果出错 */printf("error ocurred!\n");else if(pc==0){/* 子进程 */printf("This is child process with pid of %d.\n",getpid());exit(3);/* 子进程返回3 */}else{/* 父进程 */pr=wait(&status);if(WIFEXITED(status)){/* 如果WIFEXITED返回非零值 */printf("the child process %d exit normally.\n",pr);printf("the return code is %d.\n",WEXITSTATUS(status));}else/* 如果WIFEXITED返回零 */printf("the child process %d exit abnormally.\n",pr);}}
      编译并运行:

$ cc wait2.c -o wait2$ ./wait2This is child process with pid of 1838.the child process 1838 exit normally.the return code is 3.
     父进程准确捕捉到了子进程的返回值3,并把它打印了出来。

     当然,处理进程退出状态的宏并不止这两个,但它们当中的绝大部分在平时的编程中很少用到,就也不在这里浪费篇幅介绍了,有兴趣的读者可以自己参阅Linux man pages去了解它们的用法。

     实例C:

#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>int main(){   pid_t pid;   int status,i;   if(fork()= =0)  {    printf(“This is the child process .pid =%d\n”, getpid());    exit(5);  }  else{     sleep(1);     printf(“This is the parent process ,wait for child...\n”);     pid = wait(&status);     i = WEXITSTATUS(status);     printf(“child’s pid =%d. exit status=%d\n”,pid,i);  }}

    执行结果:

This is the child process.pid=1902This is the parent process .wait for child...child’s pid =1902,exit status =5
      (10) waitpid(等待子进程中断或结束)

      从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数: 

      1) pid :从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

      pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。

      pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。

      pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。         pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

     

     2) option:

      参数option 可以为0 或下面的OR 组合:
      WNOHANG 如果没有任何已经结束的子进程则马上返回,不予以等待。WUNTRACED 如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。子进程的结束状态返回后存于status,底下有几个宏可判别结束情况:

      WIFEXITED(status)如果子进程正常结束则为非0 值。

      WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。
      WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真。
      WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。
      WIFSTOPPED(status) 如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。
      WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断后才使用此宏。
      返回值如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno 中。

      options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:ret=waitpid(-1,NULL,WNOHANG| WUNTRACED);如果我们不想使用它们,也可以把options设为0,如:ret=waitpid(-1,NULL,0);如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

      而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。

      看到这里,聪明的读者可能已经看出端倪了--wait不就是经过包装的waitpid吗?

      没错,查看<内核源码目录>/include/unistd.h文件349-352行就会发现以下程序段:

static inline pid_t wait(int * wait_stat){return waitpid(-1,wait_stat,0);}
      实例:

/* waitpid.c */#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>main(){pid_t pc, pr;pc=fork();if(pc<0)/* 如果fork出错 */printf("Error occured on forking.\n");else if(pc==0){/* 如果是子进程 */sleep(10);/* 睡眠10秒 */exit(0);}/* 如果是父进程 */do{pr=waitpid(pc, NULL, WNOHANG);/* 使用了WNOHANG参数,waitpid不会在这里等待 */if(pr==0){/* 如果没有收集到子进程 */printf("No child exited\n");sleep(1);}}while(pr==0);/* 没有收集到子进程,就回去继续尝试 */if(pr==pc)printf("successfully get child %d\n", pr);elseprintf("some error occured\n");}

     编译并运行:

$ cc waitpid.c -o waitpid$ ./waitpidNo child exitedNo child exitedNo child exitedNo child exitedNo child exitedNo child exitedNo child exitedNo child exitedNo child exitedNo child exitedsuccessfully get child 1890

      父进程经过10次失败的尝试之后,终于收集到了退出的子进程。

       因为这只是一个例子程序,不便写得太复杂,所以我们就让父进程和子进程分别睡眠了10秒钟和1秒钟,代表它们分别作了10秒钟和1秒钟的工作。父子进程都有工作要做,父进程利用工作的简短间歇查看子进程的是否退出,如退出就收集它。

     (11) exit()函数

      实例:

/* exit_test1.c */#include<stdlib.h>main(){printf("this process will exit!\n");exit(0);printf("never be displayed!\n");}
    编译后运行:

$gcc exit_test1.c -o exit_test1$./exit_test1this process will exit!
     我们可以看到,程序并没有打印后面的"never be displayed!\n",因为在此之前,在执行到exit(0)时,进程就已经终止了。

     exit 系统调用带有一个整数类型的参数status,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0表示没有意外的正常结束;其他的数值表示出现了错误,进程非正常结束。理论上exit可以返回小于256的任何整数。返回的不同数值主要是给调用者作不同处理的。单独的进程是返回给操作系统的。如果是多进程,是返回给父进程的。父进程里面调用waitpid()等函数得到子进程退出的状态,以便作不同处理。根据相应的返回值来让调用者作出相应的处理。总的说来,exit()就是当前进程把控制权返回给调用该程序的程序,括号里的是返回值,告诉调用程序该程序的运行状态。我们在实际编程时,可以用wait系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。

     (12) exit和_exit

     作为系统调用而言,exit_exit是一对孪生兄弟。

     这时随便一个懂得C语言并且头脑清醒的人都会说,exit_exit没有任何区别,但我们还要讲一下这两者之间的区别,这种区别主要体现在它们在函数库中的定义。_exit在Linux函数库中的原型是:

        #include<unistd.h>        void _exit(int status);
和exit比较一下,exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中,从名字上看,stdlib.h似乎比unistd.h高级一点,那么,它们之间到底有什么区别呢?让我们先来看流程图,通过下图,我们会对这两个系统调用的执行过程产生一个较为直观的认识。
      

       从图中可以看出,_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。
       exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理I/O缓冲”一项。
       在Linux 的标准函数库中,有一套称作“高级I/O”的函数,我们熟知的printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(buffered I/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束符EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。

      请看以下例程:

      实例A:

/* exit2.c */#include<stdlib.h>main(){printf("output begin\n");printf("content in buffer");exit(0);}

      编译并运行:

$gcc exit2.c -o exit2$./exit2output begincontent in buffer
     实例B:
/* _exit1.c */#include<unistd.h>main(){printf("output begin\n");printf("content in buffer");_exit(0);}

      编译并运行:

$gcc _exit1.c -o _exit1$./_exit1output begin

    (13) return函数与exit函数的总结

    通常情况:exit(0)表示程序正常,exit(1)/exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件。用Error lookup可以查看。

     exit()结束当前进程/当前程序/,在整个程序中,只要调用exit就结束(当前进程或者在main时候为整个程序)

return()是当前函数返回,当然如果是在主函数main, 自然也就结束当前进程了,如果不是,那就是退回上一层调用。在多个进程时.如果有时要检测上进程是否正常退出的.就要用到上个进程的返回值,依次类推。

      1) 进程的开始:
      C程序是从main函数开始执行, 原型如下:
      int main(int argc, char *argv[]);
      通常main的返回值是int型, 正确返回0.
      如果main的返回值为void或者无, 某些编译器会给出警告, 此时main的返回值通常是0。

      关于main的命令行参数不做过多解释, 以下面的程序展示一下:

#include <stdio.h>int main(int argc, char *argv[]){     int i;     for (i = 0; i < argc; i++)         printf("argv[%d]: %s\n", i, argv[i]);     return 0;}

       2) 进程终止:
      C程序的终止分为两种: 正常终止和异常终止。
      正常终止分为: return, exit, _exit, _Exit, pthreade_exit。
      异常中指分为: abort, SIGNAL, 线程响应取消。
      主要说一下正常终止的前4种, 即exit系列函数.

#include <stdlib.h>    /* ISO C */void exit(int status);void _Exit(int status);#include <unistd.h>    /* POSIX */void _exit(int status);
      以上3个函数的区别是:
      exit()(或return 0)会调用终止处理程序和用户空间的标准I/O清理程序(如fclose),_exit和_Exit不调用而直接由内核接管进行清理。因此,main函数中exit(0)等价于return 0.

      3) atexit终止处理程序:
      ISO C规定, 一个进程最对可登记32个终止处理函数, 这些函数由exit按登记相反的顺序自动调用. 如果同一函数登记多次, 也会被调用多次.
      原型如下:

#include <stdlib.h>int atexit(void (*func)(void));
     其中参数是一个函数指针,指向终止处理函数,该函数无参无返回值。atexit函数本身成功调用后返回0。

     以下面的程序为例:

#include <stdlib.h>static void myexit1(){     printf("first exit handler\n");}static void myexit2(){     printf("second exit handler\n");}int main(){     if (atexit(my_exit2) != 0)         printf("can't register my_exit2\n");     if (atexit(my_exit1) != 0)         printf("can't register my_exit1\n");     if (atexit(my_exit1) != 0)         printf("can't register my_exit1\n");     printf("main is done\n");     return 0;}
     运行结果:

$ ./a.outmain is donefirst exit handlerfirst exit handlersecond exit handler

        注意上面的结果,可以发现这些函数由exit按登记相反的顺序自动调用(先myexit1后myexit2)。如果同一函数登记多次,也会被调用多次(如这里的myexit1)。而这些处理函数都是在程序推出的时候利用atexit函数调用了这些处理函数。但是如果用_exit()退出 程序,则它不关闭任何文件,不清除任何缓冲器、也不调用任何终止函数!

    (14) return函数exit函数区别

     1)  exit用于在程序运行的过程中随时结束程序,exit的参数是返回给OS的。main函数结束时也会隐式地调用 exit函数。exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且 关闭通过标准I/O函数tmpfile()创建的临时文件。exit是结束一个进程,它将删除进程使用的内存空间,同时把错误信息返回父进程,而 return是返回函数值并退出函数
     2)  return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
     3)  exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。
     4) 和机器和操作系统有关一般是   0 为正常退出  非0 为非正常退出
     5) void exit(int status);
     6) atexit() 函数的参数是一个函数指针,函数指针指向一个没有参数也没有返回值的函数。atexit()的函数原型是:int atexit (void (*)(void));在一个程序中最多可以用atexit()注册32个处理函数,这些处理函数的调用顺序与其 注册的顺序相反,也即最先注册的最后调 用,最后注册的最先调用。
     一般程序执行到 main() 的结束就完成了, 如果想在程序结束时做一些事情, 可以尝试着用这个函数。

     example:

#include #include void f1(void){    printf("exit f1\n");}void f2(void){    printf("exit f2\n");}int main(){    atexit(f1);    atexit(f2);    printf("exit main\n");    return 0;}

0 0
原创粉丝点击