第七章 进程控制开发[fork() exec exit _exit wait waitpid 守护进程]

前言:

1、fork 创建一个子进程,有两个返回值。返回0为子进程,返回大于0为父进程。

2、exec 运行新的可执行文件,取代原调用进程的数据段、代码段和堆栈段。一般是运行fork后,在子进程中执行exec。

3、exit(0)和_exit(0):exit(0)会先清理I/O缓冲后再调用系统exit,而_exit(0)是直接调用系统exit

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

5、守护进程使用在android的system下面,如netd,vold等。

====================================================================

7.1.1 Linux 进程相关基本概念

进程是一个程序的一次执行的过程。程序是静态的,进程是动态的,包括动态创建、调试和消亡的整个过程。

2、进程控制块

进程是Linux系统的基本调度单位,系统通过进程控制块描述并表示它的变化。

进程控制块包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述。

在Linux中,进程控制块中的每一项都是一个task_struct结构,它是在include/linux/sched.h中定义的。

3.进程的标识

进程号(PID, Process Idenity Number)   和  父进程号(PPID, parent process ID)

在Linux中获得当前进程的PID和PPID的系统调用函数为getpid和getppid,

用户和用户组标识、进程时间、资源利用情况等。

4.进程运行的状态 进程是程序的执行过程,根据它的生命期可以划分成3种状态

执行态/就绪态/等待态

7.1.2 Linux下的进程结构

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

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

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

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

在Linux系统中,进程的执行模式划分为用户模式和内核模式。

如果用户程序执行过程中出现系统调用或者发生中断事件,就要运行操作系统程序,变成内核模式。

7.1.4 Linux下的进程管理

进程管理分为启动进程和调度进程。

1.启动进程 

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

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

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

后台启动往往是在该进程非常耗时,且用户也不急着需要结果的时候启动的。比如格式化文本文件的进程。

(2)调度启动

费时且占用资源的维护工作,并且在深夜无人职守的时候进行,用户可以事先进行调度安排,指定任务运行的时间或者场合。

使用调度启动进程有几个常用的命令,如at命令在指定时刻执行相关进程,cron命令可以自动周期性的执行相关进程。

2.调度进程

调度进程包括对进程的中断操作、改变优先级、查看进程状态等。

ps | top | nice | renice | kill | crontab | bg

======================================================================

7.2 Linux进程控制编程

进程创建

1. fork()

pid_t fork(void);

在Linux中创建一个新进程的惟一方法是使用fork函数。它执行一次却返回两个值。

(1) fork函数说明

fork函数用于从已存在进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。

这两个分别带回它们各自的返回值,其中父进程的返回值是子进程的进程号,而子进程则返回0。因此,可以通过返回值来判断该进程是父进程还是子进程。

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

int main(void){
    pid_t result;

    result = fork();
    if(result == -1){
      perror("fork");
      exit(1);
    } else if(result == 0){
      printf("The return value is 0,In child process!! My PID is %d
",getpid());
    } else {
      printf("The return value is %d, In father process !! My PID is %d
", result, getpid());
    }
}
fork

(4)函数使用注意点

2. exec函数族

(1)exec函数族说明

fork函数用于创建一个子进程,该子进程几乎copy了父进程的全部内容,但是这个新创建的进程如何执行呢?

这个exec函数族就提供了一个在进程中启动另一个程序执行的方法,它可以根据指定的文件名或目录名找到可执行文件。

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

*当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生

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

(2)exec函数族语法 函数族里有6个以exec开头的函数

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

int execv(const char *path, char * const 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, ...)从环境变量$PATH所指出的路径中进行查找

int execvp(const char *file, char * const argv[])从环境变量$PATH所指出的路径中进行查找

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

字母为“l"(list)的表示逐个列举,其语法为char *arg;字母为"v"(vector)的表示将所有参数整体构造指针数组传递,其语法为*const argv[]。

(3)使用实例

execlp
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
    if(fork() == 0){ 
        /*call execlp, just like call "ps -ef"*/
        if(execlp("ps","ps","-ef",NULL) < 0)
            perror("execlp error!");
    }   
}
execl
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

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

3.exit和 _exit

(1)函数说明,exit和_exit函数都是用来终止进程的,进程会无条件的停止剩下的所有操作。

但这两个函数还是有区别的

_exit() 直接调用 exit系统调用

exit()->调用退出处理函数->清理I/O缓冲->调用exit系统调用

若想保证数据的完整性,就一定要使用exit()函数,因为程序处理数据有缓冲区。

由于printf函数使用的缓冲I/O方式,该函数在遇到" "换行符时自动从缓冲区中将记录读出。

如果没有" ",exit(0)能从缓冲区读出,而_exit(0)则不能。

4. wait 和 waitpid

(1)wait和waitpid函数说明

wait函数是用于使父进程(也就是调用wait的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号为止。

如果该进程没有子进程或者他的子进程已经结束,则wait就会立即返回。

waitpid的作用和wait一样,但它并不一定要等待第一个终止的子进程,它还有若干选项。wait只是waitpid的一个特例

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

pid_t wait(int *status)

pid_t waitpid(pid_t pid, int *status, int options) 

(3) waitpid使用实例

本例中首先使用fork()新建一子进程,然后让其子进程暂停5s,接下来对原有的父进程使用waitpid函数,并使用参数WNOHANG使该父进程不会阻塞。若有子进程退出,则waitpid返回子进程号;若没有子进程限出,则waitpid返回0,并且父进程每隔一秒循环判断一次。

waitpid
#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{ 
            /*调用 waitpid,且父进程不阻塞*/
            pr=waitpid(pc,NULL,WNOHANG);
            /*若子进程还未退出,则父进程暂停 1s*/
            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.
");
    }   
}

7.3 Linux守护进程

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

pid = fork();

if(pid>0)

    exit(0);父进程退出了。

子进程变成了孤儿进程,会自动由1号进程(也就是init进程)收养它。这样,原先的子进程就会变成init进程的子进程了。

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

pid_t setsid(void)

进程组:进程组是一个或多个进程的集合。进程组由进程组ID来惟一标识。除了进程号PID之后,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID,且该进程ID不会因组长进程的退出而受到影响。

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

setsid函数作用:

setsid函数用于创建一个新的会话,并担任该会话组的组长。

让进程摆脱原会话的控制。让进程摆脱原进程组的控制。让进程摆脱原控制终端的控制。

由于调用fork函数时,子进程全盘拷贝了父进程的会话期、进程组控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变。

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

让"/"作为守护进程的当前工作目录。常见函数为chdir

4.重设文件权限掩码 

umask(0)

5.关闭文件描述符

for(i = 0; i<MAXFILE; i++)

  close(i);

dameon
/*dameon.c 创建守护进程实例*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.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 Dameon
";
    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))<0){
            perror("open");
            exit(1);
        }   
        write(fd, buf, len+1);
        close(fd);
        sleep(10);
    }   
}

7.3.3 守护进程的出错处理

gdb是通过输出错误信息到控制终端来通知程序员

守护进程使用syslog服务,将程序中的出错信息输入到“/var/log/messages"系统日志文件中。

syslog是Linux中的系统日志管理服务,通过守护进程syslogd来维护。该守护进程在启动时会读一个配置文件"/etc/syslog.conf"。该文件决定了不同种类的消息会发送向何处。该机制提供了3个syslog函数,分别为openlog、syslog和closelog。

void openlog(char *ident, int option, int facility)

void syslog(int priority, char *format, ...)

void closelog(void)

(3)使用实例

7.4实验内容

原文地址:https://www.cnblogs.com/jimwind/p/2834147.html