《Linux程序设计》读书笔记------第十一章 进程

第十一章         进程和信号

UNIX标准定义进程为:

                                       一个运行着一个或者多个线程的地址空间和这些线程所需要的系统资源;

一般说来,Linux系统会在进程之间共享程序代码和系统函数,所以在任何时刻内存中都只有代码的一份副本。


1、程序区代码是以只读的形式加载到内存中,虽然不能对这个区域执行写操作,但是可以被多个进程安全的共享;

2、共享函数库的存在使可执行程序比较小

3、进程使用的变量与其他进程不同;进程的文件描述符不同;进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用与返回;进程的环境空间不同,包含专门为这个进程建立的环境变量。


4、进程表:把当前加载到内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的各类信息。操作系统通过PID对进程进行管理,PID就是进程表的索引。


5、一般而言,每个进程都是由一个我们成为父进程的启动的,被父进程启动的进程叫做子进程;Linux系统启动时,它将运行一个名为init的进程,该进程是系统运行的第一个进程,它的进程号为1;

                   你可以将init进程看做操作系统的进程管理器,它是其他所有进程的祖先进程。


6、优先级与时间片的计算:



7、启动新进程:

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

#include <stdlib.h>

int system(const char* string);

system函数的作用是,调用fork()产生子进程,由子进程运行以字符串参数的形式传递给它的命令并等待该命令的完成。命令的执行就如同在shell中执行:

$   sh  -c  string

此命令执行完毕后即返回原调用的进程;在调用system()期间SIGCHLD信号会被暂时搁置,SIGINT和SIGQUIT信号会被忽略。


如果无法启动shell来执行这个命令,system函数返回错误代码127;如果有其他错误,则返回-1;否则system函数将返回该命令的退出码。

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;
}



缺点:system很有用,但是有局限性,因为程序必须等待由system函数启动的进程结束后才能继续,因此我们不能立刻执行其他任务;

一般来说,使用system函数不是启动其他进程的理想手段,因为他必须用一个shell来启动需要的程序,由于在启动程序之前需要先启动一个shell,而且对shell的安装情况及使用的环境的依赖也很大,所以使用system函数的概率不高。




8、替换进程映像

              exec系列函数由一组相关的函数组成,它们在进程的启动方式和程序参数的表达方式上各有不同;exec函数可以把当前进程替换为一个新进程,新进程由path或者file参数指定;你可以使用exec函数将程序从一个程序切换到另一个程序,当新的程序启动后,原有的程序就不再运行了。

注意,是替换原先的进程,而不是创建完全的新进程,当某一个进程调用exec系列函数时,该进程的代码段、数据段内容完全由新程序的代码段和数据段替代;因为exec并不是创建全新的进程,所以前后的进程号等相关信息并不发生变化,exec只是使用新程序替换了当前进程的正文、数据、堆栈等


#include  <unistd.h>

int execl(const char *path,const char *arg0,...............,(char*)0);

int execle(const char *path,const char *arg0,.................,(char*)0);

int execv(const char *path,char *const argv[]);

int execve(const char *path,char *const argv[],char *const envp[]);path代表文件路径,argv为执行时的参数,传递给可执行文件,并且需要以空指针NULL结束,最后一个参数则为传递给执行文件的新环境变量数组。

int execvp(const char *file,char * const argv[]);

int execlp(const char *file,const char *arg,.....................,(char *)0);


这些函数都是通过execve实现的;

以字母p结尾的函数通过搜索PATH环境变量来查找新程序的可执行文件的路径,如果可执行文件不再PATH定义的路径中,就需要把包括目录在内的使用绝对路径的文件名作为参数传递给函数。


函数execle和execve可以通过envp传递字符串数组作为新程序的环境变量。


exec系列函数的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,即在调用进程内部执行一个可执行文件:二进制文件或者linux下可以执行的脚本文件;


exec系列函数执行成功后不会返回,因为调用进程的实体,包括代码段、数据段和堆栈等都已经被新的内容替代,只留下进程ID等一些表面上的信息保持原样;除非调用失败,它们会返回-1;从原进程的调用点继续往下执行。


例如:

char   *const ps_argv[] = {"ps","ax",0};

char   *const ps_envp[] = {"PATH=/bin:/usr/bin","TERM=console",0};


execve("/bin/ps",ps_argv,ps_envp);



9、复制进程映像

                 要想让进程同时执行多个函数,我们可以使用线程或从源程序中创建一个完全分离的进程,后者就像init进程的做法一样,而不像exec调用那样用新程序替换当前执行的线程;


                  我们可以通过fork创建一个新进程,这个系统调用复制当前进程,在进程表中创建一个新表项,新表项中的许多属性与当前进程是相同 的。新进程几乎与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、执行环境和文件描述符。


                   fork和exec函数结合在一起使用就是创建新进程所需要的一切。


头文件

#include   <unistd.h>

#include  <sys/types.h>

函数原型:

pid_t    fork(void);pid_t是一个宏定义

返回值:若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1;


函数说明:

               一个现有进程可以调用fork函数创建一个新进程;由fork创建的新进程称为子进程;fork函数被调用一次则返回两次;两次返回的唯一区别是子进程中返回0值而父进程返回子进程ID;

               子进程是父进程的副本,它将获得父进程数据空间、堆栈等资源的副本;注意:子进程特有的是上述存储空间的副本,这意味着父子进程分别享有各自的进程空间。

               UNIX将复制父进程的地址空间内容给子进程,因此子进程拥有独立的地址空间;在不同的UNIX系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现,所以在移植代码时不应该对此作出任何假设。

             

fork返回两次:

                 由于系统在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回;因此,fork函数会返回两次,一次是在父进程中返回,一次是在子进程中返回,这两次返回值是不一样的:

                                                                               《Linux程序设计》读书笔记------第十一章 进程_第1张图片

                 在调用fork之后,数据和堆栈段有两份,代码仍为一份但是这个代码段称为两个进程的共享代码段,都从fork()函数中返回,箭头表示各自的执行处,当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。


                fork 的特点就是调用一次、返回两次;在父进程中调用一次,在父进程和子进程中各返回一次;

                fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中,父子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。


分析一个小程序:

#include  <stdio.h>

#include <sys/types.h>

#include <unistd.h>


int main(void)

{

        pid_t   pid1;

        pid_t   pid2;

        pid1 = fork();

        pid2 = fork();

         printf("pid1 is %d,pid2 is %d\n",pid1,pid2);

          return 0;

}

pid1 is 2937,pid2 is 2938
hangma@ubuntu:~/test$ pid1 is 2937,pid2 is 0
pid1 is 0,pid2 is 2939
pid1 is 0,pid2 is 0

把进入main的进程称为进程1

1、当程序进入第一个fork时,会产生一个子进程,称为进程2;进程1从fork返回时返回值是进程2的ID,进程2从fork返回时,返回值是0;

2、因为父子进程执行顺序不确定,我们假设先执行父进程,因此进程1先执行,进程2后执行

(1)、进程1进入第二个fork后,又产生一个子进程,称为进程3,;进程1从第二个fork返回时返回值是进程3的ID,进程3从fork返回时返回值0;

(2)、进程1执行printf,因为进程1执行两次fork,因此产生两个子进程,分别为进程2和进程3,而且分别返回值为进程2的ID和进程3的ID,因此打印出来是进程2的ID和进程3的ID:此为进程1的执行结果

(3)、下面接着执行进程3,注意不是进程2,因为进程3是在第二个fork中产生的,虽然进程3比进程2产生的晚,但是执行的却比进程2早;进程3从第二个fork中返回值为0,因此打印的结果分别是进程2的ID和0:进程3执行的结果

3、进程1执行完毕后,进程2执行

(1)、进程2从第一个fork中返回值为0,当进入第二个fork时,产生一个子进程,称为进程4;进程2从第二个fork中返回时返回值为进程4的ID,进程4从第二个fork中返回时返回值为0;

(2)进程2是进程4的父进程,因此先执行,此时打印出来就是0和进程4的ID:这是进程2的执行结果

(3)进程4继续执行,打印结果是0和0:这是进程4的执行结果。


《Linux程序设计》读书笔记------第十一章 进程_第2张图片




10、等待一个进程

            父进程在子进程之前结束,由于子进程还在继续运行,所以得到输出结果有点乱,可以通过在父进程中调用wait函数让父进程等待子进程的结束。

#include <sys/types.h>

#include <sys/wait.h>

/*wait for a child to die.When one does,put its status in *stat_loc and return its process ID.For errors,return (pid_t) -1;

pid_t wait(int *stat_loc);


wait系统调用将暂停父进程直到它的子进程结束为止,这个调用返回子进程的ID,它通常是已经结束运行的子进程的PID;状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或者子进程中exit函数的退出码,如果stat_loc不是空指针,状态信息将被写入它所指向的位置;如果对子进程的退出码不在意的话,只想把这个子进程消灭掉,那么可以设定stat_loc为NULL;

pid = wait(NULL);

如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被只为ECHILD;


int main(void)

{

        pid_t   pid1;

        pid_t   pid2;

        pid1 = fork();

        pid2 = fork();

         printf("pid1 is %d,pid2 is %d\n",pid1,pid2);

        if(pid1 != 0)
    {
        int stat_val;
        pid_t child_pid;


        child_pid = wait(&stat_val);
        printf("Child has finished:PID = %d\n",child_pid);
    }
    if(pid2 != 0)
    {
        int stat_val2;
        pid_t child_pid2;
        child_pid2 = wait(&stat_val2);
        printf("Child 2 has finished :PID = %d\n",child_pid2);
    }

    return 0;

}

打印结果为:

pid1 is 3436,pid2 is 3437
pid1 is 3436,pid2 is 0
Child has finished:PID = -1
Child has finished:PID = 3437
pid1 is 0,pid2 is 3438
pid1 is 0,pid2 is 0
Child 2 has finished :PID = 3438
Child 2 has finished :PID = 3436


分析一下:

调用wait()函数的父进程将等待该进程的任意一个子进程结束后才继续执行,如果有多个子进程,只需要等待其中的一个进程。


(1)

if(pid1 != 0)]

{......}

共有两个进程会执行这一段,因为pid1 !=0时,是进程1和进程3,进程1是父进程,进程3和进程2是它的子进程,所以进程1需要等待进程3和进程2中任意一个执行完毕才会向下执行,根据上文所说,父进程先执行,所以进程3的产生属于进程1先于进程2执行的过程,所以进程3先于进程2执行,所以当进程3执行wait时,因为进程3没有子进程,所以返回错误,打印为PID=-1;接着进程3返回的pid2=0,那么if(pid2 !=0){}这一段不会执行,所以进程3执行完毕后,进程1接着执行,将进程3的PID打印出来;


(2)

if(pid2 != 0)

{.......}

也有两个进程会执行这一段,进程1的pid1和pid2都不为0,所以进程1执行,进程2的pid2不为0,所以进程2执行;进程1执行到这段时,需要等待进程2执行完毕后才能执行,因此进程2先执行这一段,又因为进程2执行到这一段需要等待进程4的执行,所以进程4需要先执行,进程4的pid1和pid2都为0,所以进程4不执行这两段,直接到结束,进程4执行完毕后,进程2执行,返回进程4的PID,打印出进程4的PID,进程2执行完毕;接着进程1执行,返回进程2的PID,打印进程2的PID;


总结:

这4个进程的执行完毕的先后顺序是:进程3、进程4、进程2、进程1;

因为进程3没有子进程,所以wait()返回-1;进程3执行完毕后,进程1打印进程3的PID;进程1继续执行,


int main(void)

{

        pid_t   pid1;

        pid_t   pid2;

        pid1 = fork();

        pid2 = fork();

父进程1,执行第一个fork()之后,产生子进程2;
父进程1,执行第二个fork()之后,产生子进程3;
子进程2,执行第二个fork()之后,产生子进程4;
   printf("pid1 is %d,pid2 is %d\n",pid1,pid2); 父进程1:打印进程2和进程3的PID
子进程2:打印0和子进程4的PID
子进程3:打印进程2的PID和0
子进程4:打印0和0
        if(pid1 != 0)
    {
        int stat_val;
        pid_t child_pid;


        child_pid = wait(&stat_val);
        printf("Child has finished:PID = %d\n",child_pid);
    }
父进程1和子进程3的pid1都不为0,执行此段,子进程2和子进程4的pid1=0,不执行;
父进程1:在wait()处阻塞,等待它的子进程2和子进程3的其中一个执行完毕;
                  由于进程3是在父进程中产生的,因此先于进程2执行;
                   即父进程1会在此等待子进程3执行完毕后再继续;
子进程3:执行wait(),由于子进程3没有子进程,wait()返回-1;
                   进程3执行这一段时打印-1;
父进程1:子进程3执行完毕后,父进程3执行的wait()返回子进程3的PID;
                   父进程打印子进程3的PID
   if(pid2 != 0)
    {
        int stat_val2;
        pid_t child_pid2;
        child_pid2 = wait(&stat_val2);
        printf("Child 2 has finished :PID = %d\n",child_pid2);
    }
父进程1和子进程2的pid2都不为0,执行此段,子进程3和子进程4的pid2=0,不执行
父进程1:在wait()处阻塞,等待它的子进程2执行完毕,此时子进程3已经执行完毕;
子进程2:在wait()出阻塞,等待它的子进程4执行完毕;
子进程4:不执行这一段,执行完毕;
子进程2:子进程4执行完毕后,wait()返回进程4的PID;
                    子进程2打印子进程4的PID;
                     继续执行知道完毕
父进程1:子进程2执行完毕后,wait()返回进程2的PID;
                  父进程1打印子进程2的PID

    return 0;

}

 



11、僵尸进程

                    当子进程结束后,父进程没有调用wait()或者waitpid()回收子进程占用的资源,那么子进程将会成一个僵尸进程;

                    僵尸进程就是子进程结束后的一些数据结构,保存子进程的退出码的等信息,等待父进程的调用;

                    如果父进程退出前没有调用wait()或者waitpid(),则子进程被init进程接管,子进程退出后init会回收其占用的相关资源。

                     

                     应当尽力避免僵尸进程,因为在init进程清理它们之前,这些僵尸进程会一直消耗系统资源。

避免僵尸进程的方式:

(1)父进程退出前调用waitpid()函数或者wait()函数,处理相关的子进程;

(2)结束父进程,这样僵尸进程就会被init()进程接管,进而init进程就会释放僵尸进程占据的系统资源

(3)如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。
(4)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。



12、waitpid()函数

#include <sys/wait.h>

#include <sys/types.h>

#include <unistd.h>


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

参数:

               pid:指定需要等待的子进程的PID

                                  pid =-1,waitpid将返回任意一个子进程的信息。

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

                                  pid = 0,等待同一个进程组中的任何子进程,如果子进程已经加入到别的进程组,waitpid不会对它做任何理睬。

                                  pid < -1,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

               stat_loc:如果stat_loc不是空指针,waitpid将把状态信息写到它所指向的位置;如果是NULL,则不保存子进程的退出码

                                 

               options:可以用来改变waitpid的行为,其中最有用的一个选项是WNOHANG,作用是防止waitpid调用将调用者的执行挂起。

                                   如果不想使用它,可以将options设置为0,wait()调用waitpid()函数时就是这么做的:

                                    waitpid(-1,NULL,0)这样就相当于wait(NULL);

如果父进程想周期性的检查某个特定的子进程是否终止,就可以使用:

waitpid(child_pid,(int*)0,WNOHANG);

如果子进程没有结束或者意外终止,它就返回0,否则返回child_pid;如果waitpid失败,它将返回-1并设置errno;失败的情况包括:没有子进程(errno被设置为ECHILD),调用被某个信号中断(EINTR)或选项参数无效(EINVAL);


wait实际上是调用waitpid实现的:

static inline pid_t wait(int *stat_loc)

{

       return waitpid(-1,stat_loc,0);

}

所以说wait()是等待任意一个子进程结束,顶多在保存一下该进程的结束时退出码。


wait和waitpid的区别:

waitpid等待特定子进程,而wait等待任意子进程;

waitpid提供了一个wait的非阻塞版本:即waitpid(-1,stat_loc,WNOHANG)

waitpid可以支持作业控制:waitpid(-1,stat_loc,WUNTRACED)


                    

你可能感兴趣的:(linux程序设计读书笔记)