11.创建子进程(续)、进程的终止

11.创建子进程(续)、进程的终止,第1张

创建子进程(续)

1.子进程是父进程的不完全副本,除了会复制父进程的数据区、BSS区、堆区、栈区、参数和环境变量外,包括输入输出缓冲区也会进行复制。

看看标准输出:

我们给一个printf("hello"),不给\n,会先到标准输出缓冲区的地方,从标准输出缓冲区再到显示器中

回忆标c中,缓冲区中的数据什么时候会到显示器中

        a.缓冲区中有\n

        b.缓冲区满

        c.程序结束

当我们给如下函数并运行时,就会看到没有任何输出,因为不满足任何一项

  1 #include 
  2 #include 
  3 #include 
  4 
  5 int main(void){
  6     printf("hello");
  7 
  8     for(;;);
  9     return 0
 10 }

而后,子进程会复制父进程的缓冲区中内容,则以如下代码显示看看

//子进程会复制父进程的输出缓冲区
#include
#include

int main(void){
    //setbuf(stdout,NULL);//关闭输出缓冲区
    
    printf("ABC");
    
    pid_t pid = fork();
    if(pid == -1){
        perror("fork");
        return -1;
    }

    //子进程代码
    if(pid == 0){
        printf("XYZ\n");
        return 0;
    }
    //父进程代码
    sleep(1);
    printf("\n");
    return 0;
}

因为ABC留存在缓冲区被子进程复制,遇到子进程XYZ\n才被输出出来,所以会是ABCXYZ,而父进程到下边\n才输出出ABC

 

而当我们将上边代码里setbuf(stdout,NULL);函数注释去掉,这个函数的作用是关闭缓冲区,就不会有这个情况出现了

 上述知识其实编写代码是可能一直用不到的,但是可能遇到这种错误,值得注意

2.fork函数返回后,系统内核会将父进程维护的文件描述符表也复制到子进程的进程表项中,但并不复制文件表象。

        父进程打开了一个文件,有一个文件描述符表,比如文件描述符3指向文件表项

        然后fork,打开一个子进程,会把父进程的文件描述符表也复制一份,那么子进程的文件描述符表中的文件描述符3也指向相同的文件表项

        我们写一个程序,父进程打开一个文件,写入hello world,然后打开子进程,用lseek修改文件读写位置,父进程再写入一个linux,看看结果,如果文件中是hello world! linux!,则调整位置并没有父进程的第二次写入,即不是公用同一个文件表项,如果是hello linux,则是公用同一个文件表项。执行后,发现是hello linux没错

//子进程复制父进程的文件描述符表
#include
#include
#include
#include

int main(void){
    //父进程打开文件
    int fd = open("./ftab.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
    if(fd == -1){
        perror("open");
        return -1;
    }
    //父进程写入数据 hello world
    char* buf = "hello world!";
    if(write(fd,buf,strlen(buf)) == -1){
        perror("write");
        return -1;
    }
    //父进程创建子进程
    pid_t pid = fork();
    if(pid == -1){
        perror("fork");
        return -1;
    }
    //子进程修改文件读写位置
    if(pid == 0){
        if(lseek(fd,-6,SEEK_END) == -1){
            perror("lseek");
            return -1;
        }
        close(fd);
        return 0;
    }
    //父进程再次写入数据 linux
    sleep(1);
    buf = "linux!";
    if(write(fd,buf,strlen(buf)) == -1){
        perror("linux!");
        return -1;
    }
    close(fd);
    return 0;
}

注意关闭父子的文件描述符表

 

3.孤儿进程演示

就是父进程创建子进程之后,父进程先于子进程结束,子进程会被孤儿院进程回收

编写代码看一下子进程的父进程存在时的ppid和父进程结束,子进程被收养后的ppid是多少

父进程fork子进程后,让父进程睡一秒钟,一秒里,让子进程输出自己的父进程,然后睡两秒种,此时父进程已经结束,子进程变成孤儿进程,看一下子进程现在父进程的pid

//孤儿进程演示
#include
#include

int main(void){
    printf("我是父进程,我的ID是%d\n",getpid());
    pid_t pid = fork();
    if(pid == -1){
        perror("fork");
        return -1;
    }
    //子进程代码
    if(pid == 0){
        printf("我是子进程,我的父进程是%d\n",getppid());
        sleep(2);//睡醒后,成孤儿
        printf("我是孤儿进程,我的父进程是%d\n",getppid());
        return 0;
    }
    //父进程代码
    sleep(1);

    return 0;
}

然后我多运行了几次,发现孤儿院进程一直是1689,我目前也不清楚是不是运行后就不变了? 

4.僵尸进程演示

子进程结束,父进程还在运行,并且父进程没有对子进程进程资源回收,则子进程就处于僵尸状态

我们利用两个终端进程查看,左边运行,右边通过ps -aux查看这一瞬间的进程照片

而关闭后,则父子进程都没了

 Z是僵尸的意思

进程的终止 正常终止

1、从main函数中返回可令进程终止

(1)进程是内存中的代码和数据,而线程则是执行代码的过程。每个进程可以包含一个多个线程,但至少要有一个主线程。每个线程都可以被看做是在一个独立的执行过程中调用了一个特殊的函数,谓之线程过程函数。线程开始,线程过程函数即被调用,线程过程函数一旦返回,线程即告终止。因此main函数也可以被看做是进程的主线程的线程过程函数。main函数一旦返回,主线程即终止,进程即终止,进程一旦终止,进程中的所有线程统统终止。这就是main函数的返回与其它函数的返回在本质上的区别。

(2)main函数的返回值即进程的退出码,父进程可以在回收子进程的同时获得该退出码,以了解导致其终止的具体原因。linux的a.out父进程是bash(shell)

无论是return 0还是return -1,终止都是正常终止

2.调用exit函数令进程终止

#include

void exit(int status);

        功能:令进程终止

        参数:status 进程的退出码,相当于main函数的返回值

        该函数不返回任何东西!!!

(1)虽然exit函数的参数和main函数的返回值都是int类型,但只有其中最低数位的字节可被其父进程回收,高三个字节会被忽略,因此在设计进程的退出码是最好不要超过一字节的值域范围(-128~127),当然正常结束会给0

(2)通过return语句终止进程只能在main函数中实现,但是调用exit函数终止进程可以在包括main函数在内的任何函数中使用

(3)exit函数在终止调用进程之前还会做几件收尾工作

        A. 调用实现 通过atexit或on_exit函数注册的 退出处理函数;

        B. 冲刷并关闭所有仍处于打开状态的标准I/O流

        C. 删除所有通过tmpfile函数创建的临时文件

        D.调用_exit(status);

(4)习惯上,还经常使用EXIT_SUCCESS和EXIT_FAILURE两个宏作为调用exit函数的参数,分别表示成功和失败。它们的值在多数系统中被定义成0和-1,但一般建议使用宏,这样做兼容性更好。

注册退出处理函数,又称为遗言函数(做收尾工作)

#include

int atexit(void (*function)(void));

        参数:function 函数指针,指向退出处理函数

        返回值:成功返回0,失败返回-1

        你把谁的函数的地址给这个函数指针的形参,它就把谁注册成退出处理函数,会在程序结束之前自动的调用。

        注册退出处理函数,将注册的函数的地址告知内核,

注意atexit函数本身并不调用退出处理函数,而只是将function参数所表示的退出处理函数地址,保存(注册)在系统内核的某个地方(进程表项)。待到exit函数被调用或在main函数里执行return语句时,再由系统内核根据这些退出处理函数的地址来调用它们。此过程亦称回调。

        那么作为收尾工作,比如close(fd);free(p);不能接收参数怎么办呢,于是使用下边的函数

        #include

        int on_exit(void (*function)(int,void*),void* arg);

        下为解释:!!!!

        void doit2(int status,void* arg){}

        on_exit(doit2,"老张有点饿");

                事实上,这个字符串的指针被传递给了arg,on_exit接收后,把两个数据交给了内核,告诉内核要结束,得调用doit2函数,然后内核一看doit2函数,发现它有两个参数,所以内核就去传参,第一个会传内核得退出码(return 的0/-1),第二个参数就传on_exit的第二个形参的地址

                事实上第二个参数,给任何数量任意类型的数据都行,数组,结构体,都可以

//进程的退出
#include
#include

//退出处理函数
void doit(void){
    printf("我是退出处理函数.....\n");
}

//退出处理函数
void doit2(int status,void* arg){
    printf("进程的退出码是%d\n",status);
    printf("doit2接受到:%s\n",(char*)arg);
    //这里要进行一次强转,因为接收到的是void*
}

int fun(void){
    printf("我是fun......\n");
    exit(0);
    return 10;
}

int main(void){
    //注册退出处理函数,讲函数doit地址告知内核
    atexit(doit);
    on_exit(doit2,"老张有点饿");
    printf("fun返回%d\n",fun());    
    return 0;
}

注意:后注册的先执行,先注册的后执行,也是注册退出处理函数

        参数:function函数指针,指向退出处理函数。其中第一个参数来自传递给exit函数的status参数或在main函数里执行return语句的返回值,而第二个参数则来自传递给on_exit函数的arg参数

                arg 泛型指针,将作为第二个参数传递给function所指向的退出处理函数

        返回值:成功返回0 失败返回-1

3.调用_exit/_Exit函数令进程终止

        #include

        void _exit(int status);

                参数:status 进程退出码,相当于main函数的返回值

                该函数不返回!!!

        #include

        void _Exit(int status);

                参数:status 进程退出码,相当于main函数的返回值

                该函数不返回!!!

_exit在终止调用进程之前也会做几件收尾工作,但与exit函数所做的不同。事实上,exit函数在做完它那三件收尾工作之后紧接着就会调用_exit函数

        A、关闭所有仍处于打开状态的文件描述符

        B、将调用进程的所有子进程托付给init进程收养

        C、向调用进程的父进程发送SIGCHLD(17)信号,父进程接收到这个信号之后,则知道这个进程结束了

        D、令调用进程终止运行,将status的低八位作为退出码保存在其终止状态中,每一个进程结束之后,都有一个整型变量称为进程的终止状态,它保存了退出码,子进程结束之后占有的资源,其实就是这个整型变量的终止状态,父进程收尸也是收这个,把这个释放

        如果只调_exit/_Exit,则只会做这四步,而exit的三个步骤则不会做了

异常终止

1、当进程执行了某些在系统看来具有危险性的 *** 作,或系统本身发生了某种故障或意外,内核会向相关进程发送特定的信号。如果进程无意针对收到的信号采取补救措施,那么内核将按照缺省方式将进程杀死,并视情形生成和核心转储文件(core)以备事后分析,俗称吐核。

        SIGILL(4)∶进程试图执行非法指令

        SIGBUS(7)∶硬件或对齐错误

        SIGFPE(8)∶浮点异常

        SIGSEGV(11)∶无效内存访问

        SIGPWR(30)∶系统供电不足

2.人为触发信号

        SIGINT(2) :Ctrl+C

        SIGQUIT(3) : Ctrl+\

        SIGKILL(9)∶不能被捕获或忽略的进程终止信号

        SIGTERM(15)∶可以被捕获或忽略的进程终止信号

3.向进程自己发送信号

        #include

        void abort(void);

                功能:向调用进程发送SIGABRT(6)信号,该信号默认情况下可使进程结束

                无参数,不返回!

 

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1325856.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存