进程控制:进程创建、等待、终止

相关进程概念请参考:>进程基本概念<

进程创建

进程创建被定义为通过父进程创建子进程的过程。

fork函数(copy)

函数原型:pid_t fork(void);
返回值:子进程返回0,父进程返回子进程id,出错返回-1

特点:

  1. fork函数调用一次,返回两次,子进程返回0,父进程返回子进程id(将ID返回给父进程的原因是子进程可能很多,这样方便找到该函数fork后到底是哪个子进程)

  2. 使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。

  3. 子进程被创建出来后,子进程是父进程的副本(子进程获得父进程数据空间,堆,栈的副本)

  4. 父进程和子进程都从fork执行结束后的位置继续执行

  5. 由于fork之后经常跟随者exec(程序加载函数),所以现在很多操作系统的实现并不执行一个父进程的副本,而是使用了写时拷贝(父子代码共享,父子在不写入时,数据也是共享的,当任意方试图写入时,便以写时拷贝的方式产生一个副本)

  6. fork之后父进程和子进程的执行顺序是随机的,取决于操作系统调度器

  7. 父进程和子进程的区别:
    1)fork的返回值不同
    2)进程ID,父进程ID不同
    3)子进程不继承父进程设置的文件锁
    4)子进程未处理的闹钟被清理
    5)子进程未处理的信号集,被设置为空集

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

int main(){
    int ret = fork();
    if(ret < 0){
        perror("fork");
        return 1;
    }
    else if(ret == 0){
        printf("I am child:  pid: %d, ppid %d, ret %d\n",getpid(), getppid(), ret);
    }
    else{
        printf("I am father: pid: %d, ppid %d, ret %d\n",getpid(), getppid(), ret);
    }
    sleep(60);
    return 0;
}

描述

描述

fork调用失败原因:
1. 内存不够
2. 进程数太多

关于fork的一个简单面试题:

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

int main(){
    int i = 0;
    for(; i < 2; ++i){
        fork();
        printf("- ");
    }
}
//问一共输出多少个-?

解析:在这里一共fork了两次,第二次是在第一次的基础上创建子进程,而第一次已经创建了一个子进程,所以第二次fork将会以第一次的两个进程为模板创建子进程,即第二次创建了4个子进程。大多数人会认为只有6个 - ,但不要忘了缓冲区概念,第一次fork时输出了两个 - ,但由于缓冲区没有满,所以那两个 - 并未输出到显示屏上,而是保存在缓冲区中。第二次fork是以第一次fork后的进程为模板创建子进程,所以一共会输出8个 - 。

描述

vfork函数(share)

vfork函数也是创建进程,但是与fork函数不相同的是:
1. vfork创建一个子进程,子进程与父进程共享地址空间,fork的子进程具有独立的地址空间
2. vfork并不将父进程的地址空间完全复制到子进程中
3. 子进程在调用exec函数或者exit函数之前,子进程在父进程的空间中运行
4. vfork函数保证子进程先运行,在它调用exec或exit函数之后父进程才会被恢复运行

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

int glob = 100;

int main(){
    pid_t pid;
    if((pid = vfork()) == -1){
        perror("fork");
        _exit(1);
    }
    if(pid == 0){
        glob = 200;
        printf("child glob %d\n", glob);
        sleep(5);
        _exit(0);
    }
    else{
        printf("father glob %d\n", glob);
    }
    return 0;
}

描述

子进程在父进程的地址空间中运行,所以子进程改变了父进程的变量值。

vfork创建的子进程, 直接return为什么会出现崩溃

如果在vfork中return,那这就意味main()函数return了,也就是main函数的栈帧释放了,而父子进程共享同一个栈,所以整个程序的就崩溃了。如果你在子进程中return,那么基本是下面的过程:

(1)首先子进程的main() 函数 return了。

(2)而main()函数return后,通常会调用 exit()或相似的函数(如:exitgroup())释放main函数的栈帧。

(3)这时,父进程收到子进程exit(),开始从vfork返回,但是父子进程共享同一个栈,而这个栈又被释放了,导致父进程执行失败

popen/system和fork的区别:

system和popen都是执行了类似的运行流程,大致是fork->execl->return。

(1) system在执行期间调用进程会一直等待shell命令执行完成(waitpid等待子进程结束)才返回,但是popen无须等待shell命令执行完成就返回了

(2.)system中对SIGCHLD、SIGINT、SIGQUIT都做了处理,但是在popen中没有对信号做任何的处理

(3) popen() 函数用创建管道的方式启动一个进程,并调用 shell。因为管道是被定义成单向的,所以 type 参数只能定义成只读或者只写, 不能是两者同时,结果流也相应的是只读或者只写

(4.)popen() 函数的返回值是一个普通的标准I/O流,它只能用 pclose() 函数来关闭,而不是 fclose() 函数。向这个流的写入被转化为对 command 命令的标准输入

进程等待

wait函数

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status); 

//返回值:成功返回被等待进程pid,失败返回-1
//参数:输出型参数,获取子进程退出状态,不关心则可以设置成NULL

调用wait函数时:

(1)如果有子进程在运行,那么当前父进程就处于阻塞状态
(2)如果子进程都已经终止,那么wait可立即获得子进程的终止状态(退出码,退出信息),子进程的终止状态是体现在status参数上的,另外wait还会返回所终止的子进程的标识符
(3)如果当前进程没有任何子进程,那么wait会立即返回错误(返回值为-1);
(4)如果有一个子进程终止,那么wait便返回

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

int main(){
    printf("father %d \n", getpid());
    int ret = fork();
    if(ret == 0){
        printf("child1 %d\n", getpid());
        sleep(3);
        exit(0);
    }
    ret = fork();
    if(ret == 0){
        printf("child2 %d\n", getpid());
        sleep(5);
        exit(0);
    }
    wait(NULL);       //如果此处只加一个wait,child2会成为僵尸进程,解决方案为再加一个wait
    wait(NULL);
    printf("father wait!\n");
    while(1){
        sleep(1);
    }
    return 0;
}

描述

结论:每次调用wait只能等待一个进程,所以要求wait与子进程数匹配

waitpid函数

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

参数:
(1)pid为监测子进程的标识符
(2)status:子进程的终止状态信息,如果不是空指针,则终止进程的终止信息就存放在它所指向的单元内,不关心终止状态可以将status设置成NULL
   WIFEXITED(status):若为正常终止子进程返回的状态,则为真
   WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码
(3)options:WNOHANG:若pid指定的子进程没有结束,则waitpid()返回0,不等待。若正常结束,则返回子进程id

返回值:与wait函数相同

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

int main(){
    printf("father %d \n", getpid());
    int ret = fork();
    if(ret == 0){
        printf("child1 %d\n", getpid());
        sleep(3);
        exit(0);
    }
    ret = fork();
    if(ret == 0){
        printf("child2 %d\n", getpid());
        sleep(5);
        exit(0);
    }
    while(1){
        ret = waitpid(-1, NULL, WNOHANG);
        printf("ret = %d\n", ret);
        if(ret > 0){
            printf("wait child %d\n\n", ret);
        }
        else if(ret < 0){
            break;
        }
        else{
            printf("father do work!\n\n");
        }
        sleep(1);
    }
    return 0;
}

描述

进程终止

进程退出有三个场景分别是 :

  1. 代码运行完毕,结果正确。

  2. 代码运行完毕,结果不正确。

  3. 代码异常终止

五种正常终止方式 :

  1. 在main函数内执行return 语句。它等效于调用exit

  2. 调用exit函数,exit函数是库函数,由C库定义,其操作包含调用各种终止处理程序,关闭所有标准I/O流等

  3. 调用 _exit 函数, exit 函数和 _exit 函数的不用地方就是它为进程提供了一种无需终止运行终止处理程序或信号处理程序而终止的办法

  4. 进程的最后一个线程在其启动例程中执行return语句.(这个现在大家先了解即可,关于线程的知识后面会详细分析)

  5. 进程的最后一个线程调用pthread_exit函数

三种异常终止方式 :

  1. 调用absort

  2. 当进程接收到某些信号时,如Ctrl+C

  3. 最后一个线程对“取消请求作出响应”

_exit函数(系统调用)

#include <unistd.h>
void _exit(int status);
//参数:status定义了进程的终止的状态,父进程通过wait来获取该值

虽然status是int,但仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行 $? 发现返回值是255

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

int main(){
    printf("hello");
    _exit(0);
}

exit函数(库函数)

#include <unistd.h>
void exit(int status);

exit在最后也会调用_exit 但是在调用它之前,还做了其他工作:

  1. 执行用户通过atexit或on_exit定义的清理函数

  2. 关闭所有打开的流,刷新缓冲区

  3. 调用_exit

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

int main(){
    printf("hello");
    exit(0);
}

思考:既然库函数调用最后也会进行系统调用,为什么不直接使用系统调用呢?
  当对文件进行操作时,会产生大量的数据(相对于底层驱动的系统调用所实现的数据操作单位而言),使用库函数调用可以大大减少系统调用的次数。这是因为缓冲区的存在,在用户空间和内核空间,对文件操作都使用了缓冲区,当内核缓冲区满了之后或写结束之后才会将缓冲区中的内容写到文件对应的硬件媒介中,有效提高了效率。

猜你喜欢

转载自blog.csdn.net/adorable_/article/details/80210855