2一、进程控制

  进程控制的主要任务就是系统使用一些具备特定功能的程序端来建立、撤销进程以及完成进程各状态之间的转换,从而达到多进程、高效率、并发的执行和协调,实现资源共享的目的。html

一、进程标识

  每一个进程都有惟一的、用非负整型表示的进程ID,这个ID就是进程标识符。起做用就如同身份证同样,因其惟一性,系统能够准确的定位到每个进程。进程标识符的类型是pid_t,本质是一个无符号整数。linux

  虽然是惟一的,可是进程ID是可复用的,当一个进程终止后,其ID就称为复用的候选者,大多数UNIX/Linux系统实现了延时复用算法,使得赋予新建进程的ID不一样于最近终止进程所使用的ID。这防止将新进程误认为是使用同一个ID的某个已终止的进程。算法

  一个进程标识符对应惟一进程,多个进程标识符能够对应同一个程序。所谓程序指的是可运行的二进制代码的文件,把这种文件加载到内存中运行就获得了一个进程。同一个程序文件加载屡次就会获得不一样的进程,所以进程标识符与进程之间是一一对应的,和程序是多对一的关系。shell

1

 在Linux shell中,可使用ps命令查看当前用户所使用的进程。编程

    第一列内容是进程标识符(PID),这个标识符是惟一的;最后一列内容是进程的程序文件名。咱们能够从中间找到有多个进程对应同一个程序文件名的状况,这是由于有一些经常使用的程序被屡次运行了,好比shell和vi编辑器等。数组

     每一个进程都有6个重要的ID值,分别是:进程ID、父进程ID、有效用户ID、有效组ID、实际用户ID和实际组ID。这6个ID保存在内核中的数据结构中,有些时候用户程序须要获得这些ID。网络

   例如,在/proc文件系统中,每个进程都拥有一个子目录,里面存有进程的信息。当使用进程读取这些文件时,应该先获得当前进程的ID才能肯定进入哪个进程的相关子目录。因为这些ID存储在内核之中,所以,Linux提供一组专门的接口函数来访问这些ID值。数据结构

    Linux环境下分别使用getpid()和getppid()函数来获得进程ID和父进程ID,分别使用getuid()和geteuid()函数来获得进程的用户ID和有效用户ID,分别使用getgid()和getegid()来得到进程的组ID和有效组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 getegid(void);    //获取有效组ID

  函数执行成功,返回当前进程的相关ID,执行失败,则返回-1。异步

示例:

获取当前进程的ID信息:

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

int main(int argc,char *argv[]) { pid_t pid=0,ppid=0; uid_t uid=0,euid=0; gid_t gid=0,egid=0; pid=getgid(); ppid=getppid(); uid=getuid(); euid=geteuid(); gid=getgid(); egid=getegid(); printf("当前进程ID:%u\n",pid); printf("父进程ID:%u\n",ppid); printf("用户ID:%u\n",uid); printf("有效用户ID:%u\n",euid); printf("组ID:%u\n",gid); printf("有效组ID:%u\n",egid); return 0; }

运行结果如图

二、进程建立 

   进程是Linux系统中最基本的执行单位。Linux系统容许任何一个用户建立一个子进程。建立以后,子进程存在于系统之中,而且独立于父进程。该子进程能够接受系统调度,能够分配到系统资源。系统能检测到它的存在,而且会赋予它与父进程一样的权利。

  Linux系统中,使用函数fork()能够建立一个子进程,其函数原型以下:

#include <stdio.h> pid_t fork(void);

  除了0号进程之外,任何一个进程都是由其余进程建立的。建立新进程的进程,即调用函数fork()的进程就是父进程。

  函数fork()不须要参数,返回值是一个进程的ID。返回值状况有如下三种:

(1)对于父进程,函数fork()返回新建立的子进程的ID。

(2)对于子进程,函数fork()返回0.因为系统的0号进程是内核进程,因此子进程的进程号不多是0,由此能够区分父进程和子进程。

(3)若是出错,返回-1。

fork的一个特性是父进程的全部打开文件描述符都被复制到子进程中去。在fork以后处理的文件描述符有两种常见的状况:

1. 父进程等待子进程完成。在这种状况下,父进程无需对其描述符作任何处理。当子进程终止后,子进程对文件偏移量的修改和已执行的更新。

2. 父子进程各自执行不一样的程序段。这种状况下,在fork以后,父子进程各自关闭他们不须要使用的文件描述符,这样就不会干扰对方使用文件描述符。这种方法在网络服务进程中常用。

下面经过一个示例对此函数进行了解

#include <stdio.h> #include <stdlib.h> #include <unistd.h> int global; int main(int argc,char *argv[]) { pid_t pid; int stack=1; int *heap=NULL; heap=(int*)malloc(sizeof(int)); *heap=2; pid=fork(); if(pid<0) { perror("fork()"); exit(1); } else if(pid==0)//0是第一个父进程  { global++; stack++; (*heap)++; printf("the child,data:%d,stack:%d,heap:%d\n",global,stack,*heap); exit(0); }else { sleep(2); printf("the child,data:%d,stack:%d,heap:%d\n",global,stack,*heap); exit(0); } return 0; }

程序运行结果以下:

函数fork()会建立一个新的进程,并从内核中为此进程获得一个新的可用的进程ID,以后为这个新进程分配进程空间,并将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,而且和父进程共享代码段。这时候,系统中又多出一个进程,这个进程和父进程同样,两个进程都要接受系统的调用。

  下列两种状况可能会致使fork()的出错:

(1)系统中已经存在了太多的进程。

(2)调用函数fork()的用户进程太多。

  通常系统中对每一个用户所建立的进程数是有限的,若是数量不加限制,那么用户能够利用这一缺陷恶意攻击系统。

建立共享空间的子进程

 进程在建立一个新的子进程以后,子进程的地址空间彻底和父进程分开。父子进程是两个独立的进程,接受系统调度和分配系统资源的机会均等,所以父进程和子进程更像是一对兄弟。若是父子进程共用父进程的地址空间,则子进程就不是独立于父进程的。

     Linux环境下提供了一个与fork()函数相似的函数,也能够用来建立一个子进程,只不过新进程与父进程共用父进程的地址空间,其函数原型以下:

#include <unistd.h> pid_t vfork(void);

如今经过一个示例对vfork()函数进行理解

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

int globvar = 6; int main(void) { int var; pid_t pid; var = 88; printf("before vfork\n"); if((pid = vfork()) < 0 ) { perror("vfork()"); } else if(pid == 0) {     globvar ++; var ++; _exit(0); } printf("pid = %ld, glob = %d, var = %d\n",(long)getpid(), globvar, var); exit(0); }

程序运行结果:

 

(1) vfork()函数产生的子进程和父进程彻底共享地址空间,包括代码段、数据段和堆栈段,子进程对这些共享资源所作的修改,能够影响到父进程。由此可知,vfork()函数与其说是产生了一个进程,还不如说是产生了一个线程。

(2) vfork()函数产生的子进程必定比父进程先运行,也就是说父进程调用了vfork()函数后会等待子进程运行后再运行。

下面的示例程序用来验证以上两点。在子进程中,咱们先让其休眠2秒以释放CPU控制权,在前面的fork()示例代码中咱们已经知道这样会致使其余线程先运行,也就是说若是休眠后父进程先运行的话,则第(2)点则为假;不然为真。第(2)点为真,则会先执行子进程,那么全局变量便会被修改,若是第(1)点为真,那么后执行的父进程也会输出与子进程相同的内容。代码以下:

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

int global = 1; int main(void) { pid_t pid; int   stack = 1; int  *heap; heap = (int *)malloc(sizeof(int)); *heap = 1; pid = vfork(); if (pid < 0) { perror("fail to vfork"); exit(-1); } else if (pid == 0) { //sub-process, change values
        sleep(2);//release cpu controlling
        global = 999; stack = 888; *heap  = 777; //print all values
        printf("In sub-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap); exit(0); } else { //parent-process
        printf("In parent-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap); } return 0; }

程序运行结果:

 

 

在使用vfork()函数时应该注意不要在任何函数中调用vfork()函数。下面的示例是在一个非main函数中调用了vfork()函数。该程序定义了一个函数f1(),该函数内部调用了vfork()函数。以后,又定义了一个函数f2(),这个函数没有实际的意义,只是用来覆盖函数f1()调用时的栈帧。main函数中先调用f1()函数,接着调用f2()函数。

//@file vfork.c //@brief vfork() usage
#include <stdio.h> #include <stdlib.h> #include <unistd.h>

int f1(void) { vfork(); return 0; } int f2(int a, int b) { return a+b; } int main(void) { int c; f1(); c = f2(1,2); printf("%d\n",c); return 0; }

程序运行结果:

经过上面的程序运行结果能够看出,一个进程运行正常,打印出了预期结果,而另外一个进程彷佛出了问题,发生了段错误。出现这种状况的缘由能够用下图来分析一下:

4

     左边这张图说明调用vfork()以后产生了一个子进程,而且和父进程共享堆栈段,两个进程都要从f1()函数返回。因为子进程先于父进程运行,因此子进程先从f1()函数中返回,而且调用f2()函数,其栈帧覆盖了原来f1()函数的栈帧。当子进程运行结束,父进程开始运行时,就出现了右图的情景,父进程须要从f1()函数返回,可是f1()函数的栈帧已经被f2()函数的所替代,所以就会出现父进程返回出错,发生段错误的状况。

     由此可知,使用vfork()函数以后,子进程对父进程的影响是巨大的,其同步措施势在必行。

 

三、父子进程 

   子进程彻底复制了父进程地址空间的内容。但它并无复制代码段,而是和父进程共用代码端。这样作是由于虽然因为子进程可能执行不一样的流程,会改变数据段,可是代码是只读的,不存在被修改的问题,所以可共用。

  从前面的示例中能够看出子进程对于数据段和堆栈端变量的修该并不能影响到父进程的进程环境。父进程的资源大部分能被fork()所复制,只有一小部分资源不一样于子进程。子进程继承的资源状况如表所示

 
image

 

      如今的Linux内核实现fork()函数时每每实现了在建立子进程时并不当即复制父进程的数据段和堆栈段,而是当子进程修改这些数据内容时复制才会发生,内核才会给子进程分配进程空间,将父进程的内容复制过来,而后继续后面的操做。这样的实现更加合理,对于一些只是为了复制自身完成一些工做的进程来讲,这样作的效率会更高。这也是现代操做系统中一个重要的概念——“写时复制”的一个重要体现。

 

四、进程资源回收

  当一个进程正常或异常终止时,内核会向其父进程发送SIGCHLD信号。由于子进程终止是个异步事件(这能够在父进程运行的任意时刻发生),因此这种信号也是内核向父进程发送的异步通知。父进程能够选择-忽略该信号,或者提供一个该信号发生时被调用执行的信号处理函数,对于这种信号,系统默认的是忽略它。

  linux系统提供了函数wait()和waitpid()来回收子进程资源,其函数原型以下:

#include <sys/wait.h> #include <sys/types.h> pid_t wait(int *statloc); pid_t waitpidd(pid_t pid, int *statloc, int options);

  这两个函数区别:

  • wait若是在子进程终止前调用则会阻塞,而waitpid有一选项可使调用者不阻塞。
  • waitpid并不等待第一个终止的子进程--它有多个选项,能够控制它所等待的进程。

若是调用者阻塞并且它有多个子进程,则在其一个子进程终止时,wait就当即返回。由于wait返回子进程ID,因此调用者知道是哪一个子进程终止了。
  参数statloc是一个整型指针。若是statloc不是一个空指针,则终止状态就存放到它所指向的单元内。若是不关心终止状态则将statloc设为空指针。
  这两个函数返回的整型状态由实现定义。其中某些位表示退出状态(正常退出),其余位则指示信号编号(异常返回),有一位指示是否产生了一个core文件等等。POSIX.1规定终止状态用定义在<sys/wait.h>中的各个宏来查看。有三个互斥的宏可用来取得进程终止的缘由,它们的名字都已WIF开始。基于这三个宏中哪个值是真,就可选用其余宏(这三个宏以外的其余宏)来取得终止状态、信号编号等。
  

  下面的程序中pr_exit函数使用上表中的宏以打印进程的终止状态。

 

#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.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\n", WTERMSIG(status), #ifdef WCOREDUMP WCOREDUMP(status) ? "(core file generated)" : ""); #else

        ""); #endif } 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) { fprintf(stderr, "fork error"); } else if (pid == 0) { exit(7); } if (wait(&status) != pid) { fprintf(stderr, "wait error"); } pr_exit(status); if ((pid = fork()) < 0) { fprintf(stderr, "fork error"); } else if (pid == 0) { abort(); } if (wait(&status) != pid) { fprintf(stderr, "wait error"); } pr_exit(status); if ((pid = fork()) < 0) { fprintf(stderr, "fork error"); } else if (pid == 0) { status = 8;
 } if (wait(&status) != pid) { fprintf(stderr, "wait error"); } pr_exit(status); return 0; }

编译运行结果:

  wait是只要有一个子进程终止就返回,waitpid能够指定子进程等待。对于waitpid的pid参数:

  • pid == -1, 等待任一子进程。这时waitpid与wait等效。
  • pid > 0, 等待子进程ID为pid。
  • pid == 0, 等待其组ID等于调用进程的组ID的任一子进程。
  • pid < -1 等待其组ID等于pid的绝对值的任一子进程。

  对于wait,其惟一的出错是没有子进程(函数调用被一个信号中断,也可能返回另外一种出错)。对于waitpid, 若是指定的进程或进程组不存在,或者调用进程没有子进程都能出错。   options参数使咱们能进一步控制waitpid的操做。此参数或者是0,或者是下表中常数的逐位或运算。
  

竞态条件

  当多个进程都企图对某共享数据进行某种处理,而最后的结果又取决于进程运行的顺序,则咱们认为这发生了竞态条件(race condition)。若是在fork以后的某种逻辑显式或隐式地依赖于在fork以后是父进程先运行仍是子进程先运行,那么fork函数就会是竞态条件活跃的孽生地。
  若是一个进程但愿等待一个子进程终止,则它必须调用wait函数。若是一个进程要等待其父进程终止,则可以使用下列形式的循环:

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

  这种形式的循环(称为按期询问(polling))的问题是它浪费了CPU时间,由于调用者每隔1秒都被唤醒,而后进行条件测试。
  为了不竞态条件和按期询问,在多个进程之间须要有某种形式的信号机制。在UNIX中可使用信号机制,各类形式的进程间通讯(IPC)也可以使用。
  在父、子进程的关系中,经常有如下状况:在fork以后,父、子进程都有一些事情要作。例如:父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程建立一个文件。在本例中,要求每一个进程在执行完它的一套初始化操做后要通知对方,而且在继续运行以前,要等待另外一方完成其初始化操做。这种状况能够描述为以下:

TELL_WAIT(); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { TELL_PARENT(getppid()); WAIT_PARENT(); exit(0); } TELL_CHILD(pid); WAIT_CHILD(); exit(0);

五、进程体替换

  使用函数fork()建立新的进程后,子进程每每须要调用函数exec以执行另外一个程序。当进程调用函数exec()时,该进程执行的程序彻底替换为新程序,而新程序则从其函数main()开始执行。由于调用exec并不能建立新进程,因此先后的进程ID并未改变,函数exec指示用磁盘上的一个程序替换了当前进程的正文段、数据段、堆段和栈段。

  一般有6种exec()函数可供使用,它们统称为exec()函数族,咱们可使用其中任意一个。exec()函数族使Linux系统对进程的控制更加完善。使用fork()建立新进程,使用函数exec()执行新程序,使用函数exit()和wait()终止进程和等待进程终止。exec()函数原型以下:

#include <unistd.h>

extern char **environ; int execl(const char *pathname, const char *arg0, ... /* (char *) 0 */); int execv(const char *pathname, char *const argv[]); int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */); int execve(const char *pathname, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg0, ... /* (char *) 0 */); int execvp(const char *filename, char *const argv[]);

  这些函数之间的第一个区别是前四个取路径名做为参数,后两个取文件名做为参数。当制定filename做为参数时:

  • 若是filename中包含/,则就将其视为路径名。
  • 不然按PATH环境变量。

  若是excelp和execvp中的任意一个使用路径前缀中的一个找到了一个可执行文件,可是该文件不是机器可执行代码文件,则就认为该文件是一个shell脚本,因而试着调用/bin/sh,并以该filename做为shell的输入。
  第二个区别与参数表的传递有关(l 表示表(list),v 表示矢量(vector))。函数execl、execlp和execle要求将新程序的每一个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。另外三个函数execv,execvp,execve则应先构造一个指向个参数的指针数组,而后将该数组地址做为这三个函数的参数。
  最后一个区别与向新程序传递环境表相关。以 e 结尾的两个函数excele和exceve能够传递一个指向环境字符串指针数组的指针。其余四个函数则使用调用进程中的environ变量为新程序复制现存的环境。
  六个函数之间的区别:
  

  每一个系统对参数表和环境表的总长度都有一个限制。当使用shell的文件名扩充功能产生一个文件名表时,可能会收到此值的限制。

归结起来,6个exec()函数之间的关系以下:

  执行exec后进程ID没改变。除此以外,执行新程序的进程还保持了原进程的下列特征:

  • 进程ID和父进程ID。
  • 实际用户ID和实际组ID。
  • 添加组ID。
  • 进程组ID。
  • 对话期ID。
  • 控制终端。
  • 闹钟尚余留的时间。
  • 当前工做目录。
  • 根目录。
  • 文件方式建立屏蔽字。
  • 文件锁。
  • 进程信号屏蔽。
  • 未决信号。
  • 资源限制。
  • tms_utime,tms_stime,tms_cutime以及tms_ustime值。

   对打开文件的处理与每一个描述符的exec关闭标志值有关。进程中每一个打开描述符都有一个exec关闭标志。若此标志设置,则在执行exec时关闭该文件描述符,不然该描述符仍打开。除非特意用fcntl设置了该标志,不然系统的默认操做是在exec后仍保持这种描述符打开。
  POSIX.1明确要求在exec时关闭打开目录流。这一般是由opendir函数实现的,它调用fcntl函数为对应于打开目录流的描述符设置exec关闭标志。
  在exec先后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序的文件的设置-用户-ID位和设置-组-ID位是否设置。若是新程序的设置-用户-ID位已设置,则有效用户ID变成程序文件的全部者的ID,不然有效用户ID不变。对组ID的处理方式与此相同。

示例:使用execl()进行进程体替换

 

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

int main(int argc,char *argv[]) { int count =0; pid_t pd =0; if(argc<2) { printf("Usage Error!\n"); exit(1); } for(count=1;count<argc;count++)//指令输入多少个文件,建立多少个进程
 { pd=fork(); if(pd<0) { perror("fork()"); exit(1); }else if(pd==0) { printf("Child Start PID=%d\t****\n",getpid());//建立进程成功输出当前进程PID
    execl("/bin/ls","ls",argv[count],NULL); //调用execl函数切换新进程,第一参数path字符指针所指向要执行的文件路径, 接下来的参数表明执行该文件时传递的参数列表:argv[0],argv[1]... 最后一个参数须用空指针NULL做结束。
    perror("execl"); exit(1); } else { wait();//等待当前进程终止
    printf("Child End PID=%d\t****\n\n",getpid()); } } exit(0); }

程序运行结果以下:

 

六、调用命令行

   C程序调用shell脚本共同拥有三种法子 :system()、popen()、exec系列数call_exec1.c 。其中system() 不用你本身去产生进程。它已经封装了,直接增长本身的命令,使用起来最为方便,这里重点讲解Linux下使用函数system()调用Shell命令,其函数原型以下:

#include <stdlib.h>

int system(const char *command);

  参数command是须要执行的Shell命令。函数system的返回值比较复炸,其为一个库函数,封装了fork()、exec()、和waitpid(),其函数原型以下:

int system(const char * cmdstring) { pid_t pid; int status; if(cmdstring == NULL){ return (1); } if((pid = fork())<0){ status = -1; } else if(pid == 0){ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); -exit(127); //子进程正常执行则不会执行此语句 
 } else{ while(waitpid(pid, &status, 0) < 0){ if(errno != EINTER){ status = -1; break; } } } return status; } 

  其返回值须要根据着三个函数加以区分:

若是fork()或waitpid()执行失败,函数system()返回-1.

若是函数exec()执行失败,函数system的返回值于shell调用的exit的返回值同样,表示指定文件不可执行。

若是三个文件都执行成功,函数system()返回执行程序的终止状态,其值和命令“echo $”的值是同样的。

若是参数command所指向的字符串为NULL,函数system返回1,这能够用来测试当前系统是否支持函数system。对于Linux来讲,其所有支持函数system。

  函数system()的执行效率比较低:在函数system中要两次调用函数fork()和exec(),第一次加载Shell程序,第二次加载须要执行的程序(这个程序由Shell负责加载)。可是对比直接使用fork()+exec()的方法,函数system()虽然效率较低,却有如下优势:

(1)添加了出错处理函数

(2)添加了信号处理函数

(3)调用了wait()函数,保证不会出现僵尸进程。

示例:

使用system函数调用系统命令行

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





int main(int argc,char **argv[]) { char *command=NULL; int flag=0; command=(char*)malloc(1024*sizeof(char)); memset(command,0,1024*sizeof(char)); while(1) { printf("my-cmd@"); if(fgets(command,100,stdin)!=NULL) { if(strcmp(command,"exit\n")==0) { puts("quit successful"); break; } flag=system(command); if(flag==-1) { perror("fork()"); exit(1); } memset(command,0,100); } } free(command); command=NULL; exit(0); } 

程序运行结果:

七、进程时间

  任一进程均可调用times函数以得到它本身及终止子进程的时钟时间、用户CPU时间和系统CPU时间。

#include <sys/times.h> clock_t times(struct tms *buf);
返回: 若成功则为通过的时钟时间,若出错则为-1

此函数填写由buf指向的tms结构,该结构定义以下:

struct tms { clock_t tms_utime; /* 用户CPU时间 */ clock_t tms_stime; /* 系统CPU时间 */ clock_t tms_cutime; /* 终止子进程用户CPU时间 */ clock_t tms_cstime; /* 终止子进程系统CPU时间 */ }

 

  此结构没有时钟时间。做为代替,times函数返回时钟时间做为函数值。此至是相对于过去的某一时刻度量的,因此不能用其绝对值而应该使用其相对值。例: 调用times,保存其返回值,在之后的某个时间再次调用times,重新返回的值中减去之前返回的值,此差值就是时钟时间。

  全部由次函数返回的clock_t值都用_SC_CLK_TCK(由sysconf函数返回的每秒时钟滴答数)转换成秒数。

参考连接

进程控制(1):进程标识符

进程控制(2): 进程操做

Linux系统编程(二) ------ 多进程编程

进程控制(中)

linux下怎样用c语言调用shell命令

C程序调用shell脚本共有三种方式:system()、popen()、exec系列函数

Linux system函数的执行命令并获取状态

相关文章
相关标签/搜索