zoukankan      html  css  js  c++  java
  • 进程控制开发

    进程控制开发

      文件是linux中最常见最基础的操作对象,而进程则是系统调度的单位。

    Linux下进程概述

      进程相关的基本概念

      进程的定义

      进程是一个独立的可调度的活动;进程是一个抽象实体,当执行某个任务是将要分配和释放各种资源;进程是可以并行执行的的计算部分。

      以上进程的概念都不相同,但其本质是一样的。它指出了进程是一个程序的一次执行的过程。它和程序是由本质区别的,程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;而进程是一个动态的概念,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程。它是程序执行和资源管理的最小单位。因此,对系统而言,当用户在系统中键入命令执行一个程序的时候,它将启动一个进程。

      进程控制块

      进程是Linux系统的基本调度单位,那么从系统的角度看,如何描述并表示它的变化呢?在这里,是通过进程控制块来描述的。进程控制块包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述。在Linux中,进程控制块中的每一项都是一个task_struct结构,它是在include/linux/sched.h中定义的。

      进程的标识

      在Linux中最主要的进程标识有进程号(PID,Process Idenity Number)和他的父进程号(PPID,parent process ID)。其中PID唯一地标识一个进程。PID和PPID都是非零的正整数。

      在Linux中获得当前进程的PID和PPID的系统调用函数为getpid和getppid,通常程序获得当前进程的PID和PPID可以将其写入日志文件以做备份。getpid和getppid系统调用过程如下:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
        /* 获取当前进程ID和其父进程ID */
        printf("The PID of this process is %d
    ",getpid());
        printf("The PPID of this process is %d
    ",getppid());
    }
    process.c

    运行结果:

    The PID of this process is 81555
    The PPID of this process is 78932
    process.c

    另外,进程标识还有用户和用户标识、进程时间、资源利用情况等。

      进程运行的状态

      进程是程序执行过程,根据他的生命期可以划分为3种状态。

      执行态:该进程正在占用CPU。

      就绪态:进程已经具备执行的一切条件,正在等待CPU的处理时间片。

      等待态:进程不能使用CPU,若等待事件发生则可将其唤醒。

      它们之间转换的关系图:

      Linux下的进程结构

      Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,进程之间是分离的任务,拥有各自的权利和责任。其中,每一个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。

      Linux中的进程包含3个段,分别为“数据段”、“代码段”、“堆栈段”。

      数据段:存放的是全局变量、常数以及动态数据分配的数据空间(如 malloc函数取得的空间)等。

      代码段:存放的是程序代码的数据。

      堆栈段:存放的是子程序的返回地址、子程序的参数以及程序的局部变量。

     

      

      在Linux系统中,进程的执行模式划分为用户模式和内核模式。如果当前运行的是用户程序、应用程序或者内核之外的系统程序,那么对应进程就在用户模式下运行;如果在用户程序执行过程中出现系统调用或者发生中断时间,那么就要运行操作系统(即核心)程序,进程模式就变成内核模式。在内核模式下运行的进程可以执行机器的特权指令,而且此时该进程的运行不受用户的干扰,即使是root用户也不能干扰内核模式下进程的运行。

      Linux下的进程管理

      Linux下的进程管理包括启动进程和调度进程。

      启动进程

       Linux下启动一个进程有两种主要途径:手动启动和调度启动。手工启动是由用户输入命令直接启动进程,而调度启动是指系统根据用户的设置自行启动进程。

      手工启动

      手工启动进程又可分为前台启动和后台启动。

      前台启动是手动启动一个进程的最常用方式。一般地,当用户键入一个命令如"ls -l"时,就已经启动了一个进程,并且使一个前台的进程。

      后台启动往往是在该进程非常耗时,且用户也不急着需要结果的时候启动的。比如用户要启动一个需要长时间运行的格式化文本文件的进程。为了不使整个shell在格式化过程中处于"瘫痪"状态,从后台启动整个进程是明智的选择。

      调度启动

      有时,系统需要进行一些比较费时而且占用资源的维护工作,并且这些工作适合在深夜无人值守的时候进行,这时用户就可以事先进行调度安排,指定任务运行的时间或者场合,到时候系统会自动完成这一切工作。

      使用调度启动进程有几个常用的命令,如at命令在指定时刻执行相关进程,corn命令可以自动周期性地执行相关进程,在需要使用时,可以查看相关帮助手册。

      

      调度进程

       调度进程包括对进程的中断操作、改变优先级、查看进程状态等,在Linux下可以使用相关的系统命令实现其操作,下表列出了linux中常见的调用进程的系统命令,读者在需要的时候可以自行查找其用法。

    选项 参数含义
    ps 查看系统中的进程
    top 动态显示系统中的进程
    nice 按用户指定的优先级运行
    renice 改变正在运行进程的优先级
    kill 终止进程(包括后台进程)
    crontab 用于安装、删除或者列出用于驱动cron后台进程的任务
    bg 将挂起的进程放到后台执行

    Linux进程控制编程

       进程创建

       1. fork()

      在Linux中创建一个新进程的唯一方法是使用fork函数。fork函数是Linux中一个非常重要的函数,和读者以往遇到的函数也有很大的区别,它执行一次却返回两个值。

      (1) fork函数的说明

      fork函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。这两个分别带回它们各自的返回值,其中父进程的返回值是子进程的进程号,而子进程则返回0。因此,可以通过返回值来判定进程是父进程还是子进程。

      使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。因此可以看出,使用fork函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得fork函数的执行速度并不很快。

      (2) fork函数的语法

    所需头文件

    #include <sys/types.h> /* 提供类型 pid_t的定义 */

    #include <unistd.h>

    函数原型 pid_t fork(void)
    函数返回值

    0:子进程

    子进程ID(大于0的整数):父进程

    -1:出错

       (3) fork函数使用实例

    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        pid_t result;
    
        /* 调用fork函数,其返回值为result */
        result = fork();
    
        /* 通过result的值来判断fork函数的返回情况,首先进行出错处理 */
        if(result == -1){
            perror("fork");
            exit;
        }
        /* 返回值为0 代表子进程 */
        else if(result == 0){
            printf("The return value is %d
    In child process!!
    My PID is %d
     ",result,getpid());
        }
        /* 返回值大于0 代表父进程*/
        else{
            printf("The return value is %d
    In father process!!
    My PID is %d
    ",result,getpid());
        }
    }
    fork.c

      运行, 结果:

    The return value is 82636
    In father process!!
    My PID is 82635
    The return value is 0
    In child process!!
    My PID is 82636
    fork.c

      从该实例中可以看出,使用fork函数新建了一个子进程,其中的父进程返回子进程的PID,而子进程的返回值为0。

       (4) 函数使用注意点

      fork函数使用一次就创建一个进程,所以若把fork函数放在if else 判断语句中则要小心,不能多次使用fork函数。

      小知识

       由于fork完整地拷贝了父进程的整个地址空间,因此执行速度是比较慢的。为了加快fork的执行速度,有些UNIX系统设计者创建了vfork,vfork也能创建新进程,但它不产生父进程的副本。它是通过允许父子进程可访问相同物理内存从而伪装了对进程地址空间的这是拷贝,当子进程需要改变内存中数据时才拷贝父进程。这就是著名的“写操作时拷贝”(copy-on-write)技术。

      现在很多嵌入式Linux系统的fork函数调用都是采用vfork函数的实现方式,实际上uClinux所有的多进程管理都通过vfork来实现。

      2. exec函数族

      (1) exec函数族说明

      fork函数是用于创建一个子进程,该子进程几乎拷贝了父进程的全部内容,但是,这个新创建的进程如何执行呢?这个exec函数族就提供了一个进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。

      在Linux中使用exec函数族主要有两种情况:

      1.当进程认为自己不能再为系统和用户做出任务贡献时,就可以调用任何exechanshu族让自己重生;

      2.如果一个进程想执行另一个程序,那么他就可以调用fork函数新建一个进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程。(这种情况非常普遍)

      (2) exec函数族语法

      实际上,在Linux中并没有exec()函数,而是有6个以exec开头的函数族,它们之间语法有细微差别,本书在下面会详细讲解。

    所需头文件 #inlcude<unistd.h>
    函数原型

    int execl(const char *path,const char *arg,...)

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

    int execle(const char *path,const char *arg,...,char *const envp[])

    int execve(const char *path,char *const argv[],char *const envp[])

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

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

    函数返回值 -1:出错

      这6个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这个几个方面进行比较。

      查找方式

      前4个函数的查找方式都是完成的文件目录路径,而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量"$PATH"所指出的路径中进行查找。

      参数参数方式

      exec函数族的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。

      在这里以函数名的第5位字母来区分,字母为“l”(list)的标识逐个列举的方式,其语法为char *arg;字母为“v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为 *const argv[]。可以观察execl、execle、execlp的语法与execv、execve、execvp的区别。它们具体的用法在后面的实例讲解中会举例说明。

      这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的事,这些参数必须以NULL表示结束,如果使用这个列举方式,那么要把它强制转化成一个字符指针,否则exec将会把它解释为一个整形参数,如果一个整形数的长度char*的长度不同,那么exec函数就会报错。

      环境变量

      exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以"e"(Enviromen)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量。

      

      (3) exec使用实例

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        if(fork()==0)
        {
            /* 调用execlp函数,这里相当于调用了 "ps -ef" 命令 */
            if(execlp("ps","ps","-ef",NULL)<0)
            {
                perror("execlp error!");
            }
        }
    }
    execlp.c

      在该程序中,首先使用fork函数新建一个子进程,然后在子进程使用execlp函数。可以看到,这里的参数列表就是在shell中使用命令名和选项。并且当使用文件名的方式进程查找是,系统会在默认环境变量PATH中寻找该可执行文件。

      此程序的运行结果与在Shell中直接键入命令“ps -ef”是一样的。

      接下来的示例2使用完整的文件目录来查找对应的可执行文件。注意目录必须以"/"开头,否则将其视为文件名。

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        if(fork()==0)
        {
            /* 调用execl函数,注意这里要给出ps程序所在的完成路径 */
            if(execl("/bin/ps","ps","-ef",NULL)<0)
            {
                perror("execlp error!");
            }
        }
    }
    execl.c

      运行结果与上面一致。

      

      示例3利用函数execle,将环境变量添加到新建的子进程中去,这里的"env"是查看当前进程环境变量的命令:

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        /* 命令参数列表,必须以NULL结尾 */
        char *envp[]={"PATH=/tmp","USER=$unq",NULL};
    
        if(fork()==0)
        {
            /* 调用execle函数,注意这里也要指出env的完整路径 */
            if(execle("/usr/bin/env","env",NULL,envp)<0)
            {
                perror("execle error!");
            }
        }
    }
    execle.c

      运行结果:

    PATH=/tmp
    USER=$unq
    execle.c

      最后一个示例使用execve函数,通过构造指针数组的方式来传递参数,注意参数列表一定要以NULL作为结尾标志符。其代码和运行结果如下所示:

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        /* 命令参数列表,必须以NULL结尾 */
        char *arg[]={"env",NULL};
        char *envp[]={"PATH=/tmp","USER=$unq",NULL};
    
        if(fork()==0)
        {
            if(execve("/usr/bin/env",arg,envp)<0)
            {
                perror("execve error!");
            }
        }
    }
    execve.c

    运行结果:

    PATH=/tmp
    USER=$unq
    execve.c

       (4) exec 函数族使用注意点

      在使用exec函数族时,一定要加上错误判断语句。因为exec很容易执行失败,其中最常见的原因有:

      1.找不到文件或路径,此时errno被设置为ENONET;

      2. 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;

      3. 没有对应的可执行文件的运行权限,此时errno被设置为EACCES。

      小知识:

      事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用。

      3. exit和_exit

      (1) exit 和 _exit 函数说明

      exit 和 _exit函数都是用来终止进程的。当程序执行到exit或_exit时,进程会无条件地停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。但是,这两个函数还是由区别的,这两个函数的调用如下图:

      从图中可以看出,_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干到工序。exit()函数与_exit ()函数的最大的区别就在于exit()函数在exit系统之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的"清理I/O缓冲"一项。

      由于在Linux的标准函数库中,有这一种被称作"缓冲I/O(buffered I/O)"操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区去读;同样,每次写文件的时候,也紧紧是写入内存的缓冲区,等满足一定的条件(如达到一定数量或遇到特定字符等),再讲缓冲区中的内容一次性写入文件。

      这种技术大大增加了文件读写的速度,但也为编程带来了有一点麻烦。比如有一些数据,认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这是用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,就一定要使用exit()函数。

      (2) exit 和 _exit函数语法

    所需头文件

    exit:#include<stdlib.h>

    _exit:#include<unistd.h>
    函数原型 exit: void exit(int status)
    _exit: void _exit(int status)
    函数传入值

    status 是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其他的数据表示出现了错误,进程非正常结束。

    在实际编程时,可以用wait系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理

       

      (3) exit 和 _exit 使用实例

      这两个示例比较了exit和_exit两个函数的区别。由于printf函数使用的是缓冲I/O方式,该函数在遇到" "换行符时自动从缓冲区中将记录读出。实例中就是利用这个性质来今次那个比较的。

      示例1:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        printf("Using exit ...
    ");
        printf("This is the content in buffer");
        exit(0);
    }
    exit.c

      运行结果:

    abc@pc:~/c/app$ ./exit
    Using exit ...
    This is the content in bufferabc@pc:~/c/app$
    View Code

      从输出的结果中可以看到,调用exit函数时,缓冲区中的记录也能正常输出。

    示例2

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        printf("Using exit ...
    ");
        printf("This is the content in buffer");
        _exit(0);
    }
    _exit.c

    运行结果:

    abc@pc:~/c/app$ ./_exit
    Using exit ...
    View Code

      从结果中看到,调用_exit函数无法输出缓冲区中的记录。

      小知识

      在一个进程调用了exit之后,该进程并不马上就完全消失,而是留下一个称为僵尸进程(Zombie)的数据结构。僵尸进程是一种非常特殊的进程,它几乎已经放弃了所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。

      

      4. wait 和 waipid

      (1) wait 和 waipid函数说明

      wait函数是用于使父进程(也就是调用wait的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号位置。如果该父进程没有子进程或者他的子进程已经结束,则wait就会立即返回。

      waipid的作用和wait一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的wait功能,也能支持作业控制。实际上wait函数只是waitpid函数的一个特例,在Linux内部实现wait函数时直接调用函数的就是waitpid函数。

      (2) wait 和 waitpid 函数格式说明

      

      (3)waitpid使用实例

      由于wait函数的使用较为简单,在此仅以waipid为例进行讲解。本例中首先使用fork新建一个子进程,然后让其子进程暂停5s(使用了sleep函数)。接下来对原有的父进程使用waitpid函数,并使用参数WNOHANG使该父进程不会阻塞。若有子进程退出,则waitpid返回子进程号;若没有子进程退出,则waitpid返回0,并且父进程每隔1秒循环判断一次。该程序的流程图:

      

    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        pid_t pc,pr;
        pc = fork();
        if(pc<0)
            printf("Error fork.
    ");
        /* 子进程 */
        else if(pc==0){
            /* 子进程暂停5s */
            sleep(5);
            /* 子进程正常退出 */
            exit(0);
        }else{    /* 父进程 */
            do{
                pr = waitpid(pc,NULL,WNOHANG);
                if(pr==0){
                    printf("The child process has not exited
    ");
                    sleep(1);
                }
    
            }while(pr==0);
            if(pr==pc)
                printf("Get child %d
    ",pr);
            else
                printf("some error occured.
    ");
        }
    }
    waipid.c

       运行代码,

    The child process has not exited
    The child process has not exited
    The child process has not exited
    The child process has not exited
    The child process has not exited
    Get child 75
    waitpid.c

      可见,该程序在经过5次循环之后,补货到了子进程的退出信号,具体的子进程号在不同的系统上会有所区别。

      把 pr=waitpid(pc,NULL,WNOHANG); 改成 pr=waitpid(pc,NULL,0) 或 pr=waitpid(NULL),运行结果为:Get child 76

      可见,在上述两种情况下,父进程在调用waitpid或wait之后就将自己阻塞,直到有子进程退出为止。

    Linux守护进程

       守护进程概述

      守护进程,也就是通常所说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭是终止。Linux系统有很多守护进程,大多数服务都是通过守护进程实现的,如本书在第二章中讲到的系统服务都是守护。同时,守护进程还能完成许多系统任务,例如,作业规划进程crond、打印进程lqd(这里的结尾字母d就是Daemon的意思)。

      由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个中断,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它从被执行开始运转,直到整个系统关闭时才会退出。如果想让某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。可见,守护进程是非常重要的。

      编写守护进程

      编写守护进程看似复杂,但是加上也是遵循一个特定的流程。只要将此流程掌握了,就能很方便地编写出用户自己的守护进程。下面就分4个步骤来讲解怎样创建一个简单的守护进程。在讲解的同时,会配合介绍与创建守护进程相关的几个系统函数,希望读者能很好地掌握。

      1. 创建子进程,父进程退出

      这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成一程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他的命令,从而在形式上做到了与控制终端的脱离。

      到这里,有心的读者可能会问,父进程创建子进程,而父进程又退出之后,此时孩子进程不就没有父进程了吗?守护进程中确实会出现这么一个有趣的现象,由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程。在Linux中,每当系统发现一个孤儿进程,就会自动由1号进程(也就是init进程)收养它,这样,原先的子进程就会变成init进程的子进程。关键代码如下所示:

    /* 父进程退出 */
    pid = fork();
    if(pid==0){
      exit(0);  
    }
    View Code

      

      2. 在子进程中创建新会话

      这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常重大。在这里使用的是系统函数setsid,在具体介绍setsid之前,读者首先要了解两个概念:进程组和会话期。

      进程组

      进程组是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。

      每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程ID不会因组长进程的退出而受到影响。

      会话期

      会话组是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用于退出,在此期间该用户运行的所有进程都属于这个会话期,它们的关系如图:

     

      setsid

       (1) setsid函数的作用

      setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid有下面3个作用。

      1. 让进程摆脱原会话的控制。

      2. 让进程摆脱原进程组的控制。

      3. 让进程摆脱原控制终端的控制。

      那么,在创建守护进程是为什么要调用setsid函数呢?读者可以回忆一下创建守护进程的第一步,在那里调用了fork函数来创建子进程再讲父进程退出。由于在调用fork函数时,子进程全盘拷贝了父进程的会话期,进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,还不是真正意义上独立开来,而setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

      (2) setsid函数格式

    所需头文件

    #include<sys/types.h>

    #include<unistd.h>

    函数原型 pid_t setsid(void)
    函数返回值

    成功:该进程组ID

    出错:-1

       

       3. 改变当前目录为根目录

      这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统(比如“/mnt/usb”)是不能卸载的,这对以后的使用会在成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前的工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir。

      4. 重设文件权限掩码

      文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有一个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给孩子进程带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常使用的方法为umask(0)。

      5. 关闭文件描述符

      同文件权限掩码一样,用fork函数新建的子进程会从父进程那里继承了一些已经打开了的文件。这些打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法写下。

      在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中的常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2的3个文件(常说的输入、输出和报错这3个文件)已经失去了存在的价值,也应被关闭。通常按如下方式关闭文件描述符:

    for(i=0.i<MAXFILE;i++)
        close(i);
    View Code

      这样,一个简单的守护进程就建立起来了。创建守护进程的流程图如图:

      

      下面是实现守护进程的一个完整实例:

      该实例首先建立一个守护进程,然后让该守护进程每隔10s在/tmp/daemon.log中写入一句话。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    
    
    #define MAXFILE 65535
    
    int main()
    {
        pid_t pc;
        int i,fd,len;
        char *buf="This is a Daemon
    ";
        len = strlen(buf);
    
        /* 第一步 */
        pc = fork();
        if(pc<0){
            printf("error fork
    ");
            exit(1);
        }else if(pc>0)
            exit(0);
    
        /* 第二步 */
        setsid();
    
        /* 第三步 */
        chdir("/");
    
        /* 第四步 */
        umask(0);
    
        /* 第五步 */
        for(i=0;i<MAXFILE;i++)
            close(i);
    
        /* 这时创建完守护进程,以下开始正式进入守护进程工作 */
        while(1){
            if((fd = open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))){
                perror("open");
                exit(1);
            }
            write(fd,buf,len+1);
            close(fd);
            sleep(10);
        }
    }
    daemon.c

      运行结果:

    abc@pc:~/c/app$ tail -f /tmp/dameon.log
    This is a Daemon
    This is a Daemon
    This is a Daemon
    This is a Daemon
    This is a Daemon
    This is a Daemon
    This is a Daemon
    This is a Daemon
    This is a Daemon
    This is a Daemon
    .......
    
    
    abc@pc:~/c/app$ ps -ef|grep dameon
    abc        2173      1  0 10:01 ?        00:00:00 ./dameon
    abc        2184   1877  0 10:02 pts/18   00:00:00 grep --color=auto dameon
    View Code

      守护进程的出错处理

      在前面编写守护进程的具体调试过程中会发现,由于守护进程完全脱离了控制终端,因此,不能像其他进程的程序一样通过输出错误信息到控制终端来通知程序员,计时使用gdb也无法正常调试。那么,守护进程如何调试呢?一种通用的办法是使用syslog服务,将程序中的出错信息输入到"/var/log/message"系统日志文件中,从而可以直观地看到程序的问题所在。

      注意:"/var/log/message"系统日志文件只能由拥有root权限的超级用户查看。

      syslog是Linux中的系统日志管理服务,通过守护进程syslogd来维护。该守护进程在启动时会读一个配置文件"/etc/syslog.conf"。该文件决定了不同种类的信息会发向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可记录到一个文件中。

      该机制提供了3个syslog函数,分别为openlog、syslog和closelog。下面就分别介绍这3个函数。

      (1) syslog函数说明

      通常,openlog函数用于打开系统日志服务的一个连接;syslog函数是用于向日志文件中写入消息,在这里可以规定消息的优先级、消息的输出格式等;closelog函数是用于关闭系统日志服务的连接。

      (2) syslog函数格式

      openlog函数的语法

    所需头文件 #include<syslog.h>
    函数原型 void openlog(char *ident,int option,int facility)
    函数传入值 ident 要想每个消息接加入的字符串,通常为程序的名称
    option LOG_CONS:如果消息无法送到系统日志服务,则直接输出到系统控制端
    LOG_NDELAY:立即打开系统日志服务的连接。在正常情况下,直到发送到第一条消息是才打开连接。
    LOG_PERROR:将消息也同时发送到stderr上
    LOG_PID:在每条消息中包含进程PID
    facility:指定程序发送的消息类型 LOG_AUTHPRIV:安全、授权讯息
    LOG_CORN:时间守护进程(cron及at)
    LOG_DAEMON:其他系统守护进程
    LOG_KERN:内核信息
    LOG_LOCAL[0~7]:保留
    LOG_LPR:行打印机子系统
    LOG_MAIL:邮件子系统
    LOG_NEWS:新闻子系统
    LOG_SYSLOG:syslogd内部所产生的信息
    LOG_USER:一般使用者等级讯息
    LOG_UUCP:UUCP子系统

       syslog函数的语法规范:

    所需头文件 #inlcude<syslog.h>
    函数原型 void syslog(int priority,char *format,...)
    函数传入值 priority:指定消息的重要性 LOG_EMERG:系统无法使用
    LOG_ALERT:需要立即采取措施
    LOG_CRIT:有重要情况发生
    LOG_ERR:有错误发生
    LOG_WARING:有警告发生
    LOG_NOTICE:正常情况,但也是重要情况
    LOG_INFO:信息消息
    LOG_DEBUG:调试信息
    format 以字符串指针的形式表示输出的格式,类似printf中的格式

       closelog函数的语法规范

    所需头文件 #include<syslog.h>
    函数原型 void closelog(void)

       (3) 使用实例

      这里将上一节中的实例程序用syslog服务进行重写。

    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <syslog.h>
    #include <sys/wait.h>
    #include <stdlib.h>
    
    #define MAXFILE 65535
    
    int main()
    {
        pid_t pc,sid;
        int i,fd,len;
        char *buf="This is a Dameon
    ";
        len = strlen(buf);
    
        pc = fork();
        if(pc<0){
            printf("error fork!
    ");
            exit(1);
        }else if(pc>0)
            exit(0);
    
        /* 打开系统日志服务,openlog */
        openlog("demo_updata",LOG_PID,LOG_DAEMON);
        if((sid=setsid())<0){
            syslog(LOG_ERR,"%s
    ","setsid");
            exit(1);
        }
        if((sid=chdir("/"))<0){
            syslog(LOG_ERR,"%s
    ","chdir");
            exit(1);
        }
        umask(0);
        for(i=0;i<MAXFILE;i++)
            close(i);
    
        while(1){
            /* 打开守护进程的日志文件,并写入open的日志记录 */
            if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0){
                syslog(LOG_ERR,"%s
    ","open");
                exit(1);
            }
            write(fd,buf,len+1);
            close(fd);
            sleep(10);
        }
        closelog();
        exit(0);
    
    }
    syslog_dema.c

    实验内容

      编写多进程程序

      1.实验目的:通过编写多进程程序,熟练掌握fork,exec、wait、waipid等函数的使用,进一步理解在Linux中多进程编程的步骤。

      2.实验内容:该实验有3个进程,其中一个为父进程,其余两个是该父进程创建的子进程,其中一个子进程运行“ls -l”指令,另一个子进程在暂停5s之后异常退出,父进程并不阻塞自己,并等待子进程的退出信息,待收集到该信息,父进程近返回。

      3.实验步骤:

      (1)画出流程图

      (2)实验源代码

      (3)在宿主机上编译调试该程序

      (4)在确保没有编译错误之后,使用交叉编译该程序

      (5)将生成的可执行程序下载到目标板。

      

    (1)

    (2)源代码

    #include <sys/types.h>
    #include <stdio.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    
    int main(void)
    {
        pid_t child1,child2,child;
    
        /* 创建两个子进程 */
        child1 = fork();
        child2 = fork();
    
        /* 子进程1的出错处理 */
        if(child1==-1){
            perror("child1 fork");
            exit(1);
        }
        /* 在进程1中调用execlp函数 */
        else if(child1==0){
            printf("In child1:execute 'ls -l'
    ");
            if(execlp("ls","ls","-l",NULL)<0){
                perror("child1 execlp");
            }
        }
    
        /* 子进程2的出错处理 */
        if(child2==-1){
            perror("child2 fork");
            exit(1);
        }
        /* 在子进程2中使其暂停5s */
        else if(child2==0){
            printf("In child2:sleep for 5 seconds and the exit
    ");
            sleep(5);
            exit(0);
        }
        /* 在父进程中等待子进程2的退出 */
        else{
            printf("In father process:
    ");
            do{
                child = waitpid(child2,NULL,WNOHANG);
                if(child==0){
                    printf("The child2 process has not exited!
    ");
                    sleep(1);
                }
            }while(child==0);
    
            if(child==child2)
                printf("Get child2
    ");
            else
                printf("Error occured!
    ");
        }
            
    }
    exc.c

    (3)  运行结果

    abc@pc:~/c/app$ ./exc
    In father process:
    The child2 process has not exited!
    In child2:sleep for 5 seconds and the exit
    In child1:execute 'ls -l'
    total 184
    -rwxrwxr-x 1 abc abc 9168 7月   5 09:50 dameon
    -rw-rw-r-- 1 abc abc  744 7月   5 09:50 dameon.c
    -rwxrwxr-x 1 abc abc 8912 7月   5 11:35 exc
    In child1:execute 'ls -l'
    total 184
    -rw-rw-r-- 1 abc abc  997 7月   5 11:35 exc.c
    -rwxrwxr-x 1 abc abc 8704 7月   4 15:37 execl
    -rw-rw-r-- 1 abc abc  231 7月   4 15:36 execl.c
    -rwxrwxr-x 1 abc abc 8768 7月   4 15:58 execle
    -rw-rw-r-- 1 abc abc  316 7月   4 15:58 execle.c
    -rw-rw-r-- 1 abc abc 1928 7月   4 15:52 execle.o
    -rwxrwxr-x 1 abc abc 8712 7月   4 15:29 execlp
    -rw-rw-r-- 1 abc abc  224 7月   4 15:29 execlp.c
    -rwxrwxr-x 1 abc abc 8768 7月   4 16:07 execve
    -rw-rw-r-- 1 abc abc  282 7月   4 16:07 execve.c
    -rwxrwxr-x 1 abc abc 8704 7月   4 16:38 exit
    -rwxrwxr-x 1 abc abc 8704 7月   4 16:38 _exit
    -rw-rw-r-- 1 abc abc  137 7月   4 16:38 _exit.c
    -rw-rw-r-- 1 abc abc  136 7月   4 16:38 exit.c
    -rwxrwxr-x 1 abc abc 8760 7月   4 13:52 fork
    -rw-rw-r-- 1 abc abc  558 7月   4 13:52 fork.c
    -rw-rw-r-- 1 abc abc  237 7月   4 11:36 process.c
    -rwxrwxr-x 1 abc abc 9224 7月   5 10:59 syslog_dema
    -rw-rw-r-- 1 abc abc  927 7月   5 10:59 syslog_dema.c
    -rwxrwxr-x 1 abc abc 8864 7月   4 17:45 waitpid
    -rw-rw-r-- 1 abc abc  537 7月   4 17:45 waitpid.c
    -rwxrwxr-x 1 abc abc 9168 7月   5 09:50 dameon
    -rw-rw-r-- 1 abc abc  744 7月   5 09:50 dameon.c
    -rwxrwxr-x 1 abc abc 8912 7月   5 11:35 exc
    -rw-rw-r-- 1 abc abc  997 7月   5 11:35 exc.c
    -rwxrwxr-x 1 abc abc 8704 7月   4 15:37 execl
    -rw-rw-r-- 1 abc abc  231 7月   4 15:36 execl.c
    -rwxrwxr-x 1 abc abc 8768 7月   4 15:58 execle
    -rw-rw-r-- 1 abc abc  316 7月   4 15:58 execle.c
    -rw-rw-r-- 1 abc abc 1928 7月   4 15:52 execle.o
    -rwxrwxr-x 1 abc abc 8712 7月   4 15:29 execlp
    -rw-rw-r-- 1 abc abc  224 7月   4 15:29 execlp.c
    -rwxrwxr-x 1 abc abc 8768 7月   4 16:07 execve
    -rw-rw-r-- 1 abc abc  282 7月   4 16:07 execve.c
    -rwxrwxr-x 1 abc abc 8704 7月   4 16:38 exit
    -rwxrwxr-x 1 abc abc 8704 7月   4 16:38 _exit
    -rw-rw-r-- 1 abc abc  137 7月   4 16:38 _exit.c
    -rw-rw-r-- 1 abc abc  136 7月   4 16:38 exit.c
    -rwxrwxr-x 1 abc abc 8760 7月   4 13:52 fork
    -rw-rw-r-- 1 abc abc  558 7月   4 13:52 fork.c
    -rw-rw-r-- 1 abc abc  237 7月   4 11:36 process.c
    -rwxrwxr-x 1 abc abc 9224 7月   5 10:59 syslog_dema
    -rw-rw-r-- 1 abc abc  927 7月   5 10:59 syslog_dema.c
    -rwxrwxr-x 1 abc abc 8864 7月   4 17:45 waitpid
    -rw-rw-r-- 1 abc abc  537 7月   4 17:45 waitpid.c
    The child2 process has not exited!
    The child2 process has not exited!
    The child2 process has not exited!
    The child2 process has not exited!
    Get child2
    View Code

      因为几个子进程额执行有竞争关系,因此,结果中的顺序没有完全按照程序输出。可以思考怎样保证子进程的执行顺序?

    编写守护进程

      1.实验目的:通过编写一个完成的守护进程,是读者掌握守护进程编写和调试的方法,并且进一步熟悉写多进程程序。

      2.实验内容:在该实验中,读者首先建立起一个守护进程,然后在该守护进程中新建一个子进程,该子进程暂停10s,然后自动退出,并由守护进程收集子进程退出的消息。在这里,子进程和守护进程的退出消息都在"/var/log/message"中输出。子进程退出后,守护进程循环暂停,其间隔时间为10s。

      3.实验步骤

      (1)画流程图

      (2)源代码

      (3)有些嵌入式开发板没有syslog服务,读者可以在宿主机上编译运行。

      (4)运行该程序

      (5)等待10s后,以root身份查看"/var/log/message"文件。

      (6)使用 ps -ef|gerp exc2 查看该守护进程是否在运行

      4.实验结果

    (1)

     

    (2)

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <syslog.h>
    #include <sys/stat.h>
    
    #define MAXFILE 65535
    int main(void)
    {
        pid_t child1,child2;
        int i;
    
        child1 = fork();
        if(child1==-1){
            perror("child1 fork");
            exit(1);
        }else if(child1>0)
            exit(0);
    
        /* 打开日志服务 */
        openlog("exc2_info",LOG_PID,LOG_DAEMON);
        /* 以下几步是编写守护进程的常规步骤 */
        setsid();
        chdir("/");
        umask(0);
        for(i=0;i<MAXFILE;i++)
            close(i);
    
        /* 创建子进程2 */
        child2 = fork();
        if(child2==-1){
            perror("child2 fork");
            exit(1);
        }else if(child2==0){
            syslog(LOG_INFO,"child2 will sleep for 10s");
            sleep(10);
            syslog(LOG_INFO,"child2 is going to exit!");
            exit(0);
        }else{
            waitpid(child2,NULL,0);
            syslog(LOG_INFO,"child1 noticed that child2 has exited");
            closelog();
            while(1){
                sleep(10);
            }
        }
    }
    exc2.c

    实验结果:

    在'/var/log/syslog'中

    ......
    Jul  5 12:22:57 pc exc2_info[4079]: child2 will sleep for 10s
    Jul  5 12:23:07 pc exc2_info[4079]: child2 is going to exit!
    Jul  5 12:23:07 pc exc2_info[4078]: child1 noticed that child2 has exited
    /var/log/syslog

      从时间戳里可以看到child2确实暂停了10s。

    使用 ps -ef|grep exc2 命令查看,得到结果:

    abc@pc:~/c/app$ ps -ef|grep exc2
    abc        4078      1  0 12:22 ?        00:00:00 ./exc2
    abc        4086   3621  0 12:25 pts/4    00:00:00 grep --color=auto exc2
    ps -ef|grep exc2

      可见,exc2确实一直在运行。

    本章小结

      本章主要介绍进程的控制开发,首先给出了进程的基本概念,Linux下的进程的基本结构、模式与类型以及Linux进程管理。进程是Linux中程序运行和资源管理的最小单位,对进程的处理也是嵌入式Linux应用编程的基础,因此,一定要牢牢掌握。

      接下来,本章具体讲解了进程控制编程,主要讲解了fork函数和exec函数族,并且举实例加以区别,exec函数族较为庞大,希望读者能够仔细比较它们之间的区别,认真体会并理解。

      最后,本章讲解了Linux守护进程的编写,包括守护进程的概念,编写守护进程的步骤以及守护进程的出错处理。由于守护进程非常特殊,因此,在编写时由不少的区别需要特别注意。守护进程的编写实际上涉及进程控制编程的很多部分,需要加以综合应用。

  • 相关阅读:
    多说社交评论插件学习《一》
    《转》每天起床时,优秀创业者都会问自己这3个问题
    老赵面试题参考答案(二)《转》
    老赵面试题参考答案(一)《转》
    20个开源项目托管站点推荐
    .NET面试题(二)
    .NET面试题(一)
    Python笔记(28)-----继承
    pytorch实战(2)-----回归例子
    深度学习之入门Pytorch(1)------基础
  • 原文地址:https://www.cnblogs.com/doitjust/p/11132064.html
Copyright © 2011-2022 走看看