一、进程创建

1、进程

  • 进程由内核数据结构和进程的代码和数据构成,内核数据结构由操作系统提供,进程的代码和数据一般从磁盘中加载,也就是程序加载之后的结果。
  • 创建子进程时,代码一般是只能读取而不能被修改(写)的,所以父子进程可以共享代码;但数据可能会被修改,所以,会采用写时拷贝的方法,在修改的时候对父子进程的数据做分离操作。

2、fork函数

(1)概念

通过复制调用该函数的进程来创建新进程,新创建的流程称为调用该函数的进程的子进程,是调用该函数的进程的精确副本。
在这里插入图片描述

(2)创建进程执行到内核中的fork代码时,内核做的操作

  • 分配新的内存块和内核数据结构给子进程。
  • 将父进程部分数据结构内容拷贝至子进程。
  • 将子进程添加到系统进程列表当中。
  • fork函数返回,调度器开始调度。

(3)返回值

  • 如果创建进程成功,子进程将会返回0,而父进程返回的是子进程的pid。
  • 如果创建进程失败,则在父进程中返回-1,不会创建子进程,并正确地设置errno。

3、常规用法

  • 父进程通过复制自己来创建子进程,父子进程在同一份代码中,同时执行不同的代码段。
  • 父进程通过复制自己来创建子进程,父进程正常执行后续代码,子进程执行另一个不同的程序。

4、代码

#include<stdio.h>                                                                                             
#include<unistd.h>
                      
int main()      
{   
   pid_t id = fork();
   if(id < 0)
   {                                                               
       perror("fork");
       return 1;
   }
   else if(id == 0)
   {
       printf("child, pid = %d, ppid = %d\n", getpid(), getppid());
   }
   else
   {
       printf("father, pid = %d, ppid = %d\n", getpid(), getppid());
       sleep(2);
   }
    return 0;
}  

5、执行结果

在这里插入图片描述

二、进程终止

1、进程的退出结果

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2、常见退出方法

(1)正常终止

  • main函数返回
  • 调用exit函数
  • 调用_exit函数

(2)异常退出

  • 程序执行时输入ctrl + c
  • 信号终止

3、exit和_exit函数

(1)函数

在这里插入图片描述
在这里插入图片描述

  • status定义了进程的终止状态。

(2)区别

在这里插入图片描述

  • _exit函数是系统调用,而exit函数是库函数,即exit函数会调用_exit函数,只不过exit函数在调用前会执行一些操作。

4、代码

int sum(int top)
{
    int sum = 0;
    int i = 1;
    for(i = 1;i <= top; ++i)
    {
        sum += i;
    }
    //exit(1);

    return sum;
}                                                                                                             

int main()
{
    printf("hello snowdragon1\n");
    printf("hello snowdragon1\n");
    printf("hello snowdragon1\n");

    int ret = sum(100);
    printf("%d\n", ret);
    
    printf("hello snowdragon2\n");
    printf("hello snowdragon3\n");
    printf("hello snowdragon4\n");
    
    exit(1);
                                                                                                              
    printf("hello snowdragon5\n");
    printf("hello snowdragon6\n");
    printf("hello snowdragon7\n");
    
    return 0;
}

5、运行结果

在这里插入图片描述

  • 下方为删除sum函数中exit函数的注释后的运行结果

在这里插入图片描述

三、进程等待

1、作用

  • 当子进程退出后,如果父进程不管它,就可能造成僵尸进程问题,进而造成内存泄漏。而进程一旦变成僵尸状态,我们就无法通过信号之类的方法去杀死它,因为无法杀死一个已经死去的进程。
  • 我们需要知道父进程派给子进程的任务完成的怎样,而当父进程进行进程等待时,如果子进程正常退出,父进程就能得知该类信息,获取子进程的退出信息。
  • 父进程可以通过进程等待的方式,回收子进程资源。

2、方法

(1)wait和waitpid

在这里插入图片描述

(2)参数和返回值

【1】pid
  • 当pid为-1时,表示等待任意一个子进程。此时,当waitpid函数的options为0时,其与wait函数的作用相同。
  • 当pid大于0时,表示等待进程id与pid相等的子进程。
【2】options
  • 当options为0时,表示父进程阻塞等待子进程。父进程一般是在内核中阻塞,等待被唤醒。
  • 当options为WNOHANG时,若pid指定的子进程没有结束(退出),则waitpid函数返回0,不予以等待。若pid指定的子进程正常结束,则返回该子进程的id。
【3】返回值
  • 当正常返回时,返回收集到的子进程的进程id。
  • 如果waitpid函数设置了选项options为WNOHANG,而调用中waitpid函数发现没有已退出的子进程可收集,则返回0。
  • 如果调用时出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。

(3)status

【1】概念

status参数是一个输出型参数,由操作系统填充。如果传递给它的实参为NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

【2】分布

在这里插入图片描述

【3】两个宏
  • WIFEXITED(status):表示子进程返回的状态,若子进程为正常终止,则为真,否则为假。
  • WEXITSTATUS(status):若WIFEXITED非零(为真),则提取子进程的退出码。

3、代码

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt--)                                                                                          
        {
            printf("子进程%d, pid = %d, ppid = %d\n", cnt, getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        printf("父进程,pid = %d, ppid = %d\n", getpid(), getppid());
        int status = 0;
        waitpid(id, &status, 0);
        
        //printf("等待子进程成功:%d, 子进程退出信号编号:%d,子进程退出码: %d\n", id, status & 0x7f, (status >> 8) & 0xff);
        printf("等待子进程成功:%d, 子进程退出信号编号:%d,子进程退出码: %d\n", id, WIFEXITED(status), WEXITSTATUS(status));
    }

    return 0;
}

4、运行结果

在这里插入图片描述

5、使用wait和waitpid函数的原因

  • 进程具有独立性,父子进程之间的数据会发生写时拷贝,如果采用全局变量之类的方法,父进程将无法拿到子进程的退出结果(status)。
  • 僵尸进程至少会保留该进程的PCB信息,即task_struct里面会保留所属进程退出时的退出结果信息,而父进程调用wait和waitpid函数的本质是读取子进程的task_struct结构中两个保存退出信息的变量。
  • 虽然进程具有独立性,而进程退出码是进程的数据。但是,wait和waitpid函数是系统调用。所以,父进程调用wait和waitpid函数后所进行的操作是由操作系统执行的。

四、进程程序替换

1、概念

  • 程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据)到调用进程的地址空间中。
  • 进程将磁盘上新的程序加载到内存后,会和该进程的页表重新建立映射关系。

2、替换原理

  • 用fork创建子进程后,子进程可以调用exec系列函数的一种以执行另一个程序。
  • 当进程调用exec系列函数的一种时,该进程用户空间中的代码和数据完全被新程序替换,从新程序的启动例程开始执行。
  • 调用exec系列函数并不会创建新的进程,所以进程在调用exec系列函数前后,该进程的id并不会改变。

3、exec系列函数

在这里插入图片描述
在这里插入图片描述

  • 这些函数如果调用成功则加载新的程序,从启动代码开始执行,不会有返回值。
  • 如果调用出错则返回-1,所以,exec系列函数只有调用出错时才会有返回值。
  • 第二张的execve函数是系统调用。即第一张图片的六个函数最终都会调用execve函数。

4、代码

    1 #include<stdio.h>                                                                                           
    2 #include<unistd.h>
    3 #include<sys/types.h>
    4 #include<sys/wait.h>
    5 #include<stdlib.h>
    6 
    7 #define NUM 32
    8 #define SIZE 16
    9 
   10 int main()
   11 {
   12     char* _argv[NUM] = {
   13         (char*)"ls",
   14         (char*)"-l",
   15         (char*)"-a",
   16         NULL
   17     };
   18     char* const _envp[SIZE] = {              //_envp的元素是char*类型的,_envp可以加const修饰,也可以不加
   19         (char*) "snowdragon=writed 666", //字符串=前面不要加空格,否则它的名字就变了,即需要加空格才是变量名
   20         NULL
   21     };
   22     pid_t id = fork();
   23     if(id == 0)
   24     {
   25         //子进程
   26         printf("子进程,pid:%d\n", getpid());
   27         sleep(1);
   28 
   29         //_envp是用来存储环境变量的,而不是存储选项的。选项不要加-
   30         execle("./mycmd", "./mycmd", "a", NULL, _envp);
   31         //execlp("ls", "ls", "-l", "-a", NULL);
   32         //execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
   33         //execv("/usr/bin/ls", _argv);
   34         //execvp("ls", _argv);
   35         exit(1);
   36     }
   37     else                                                                                                    
   38     {
   39         printf("父进程,pid:%d\n", getpid());
   40         int status = 0;
   41         pid_t res = waitpid(id, &status, 0);
   42         if(res > 0)
   43         {
   44             printf("等待子进程成功,退出码:%d\n", WEXITSTATUS(status));
   45         }                                                                                                   
   46     }
   47     return 0;
   48 }
  1 #include<stdio.h>
  2 #include<stdlib.h>                                                                                            
  3 #include<string.h>
  4 
  5 int main(int argc, char* argv[])        //argv是指针数组
  6 {
  7     if(argc != 2)
  8     {
  9         printf("%d\n", argc);
 10         printf("程序执行失败!\n");
 11         exit(1);
 12     }
 13 
 14     printf("snowdragon:%s\n", getenv("snowdragon"));        //环境变量名需要与传入的环境变量名相同才能获取到
 15 
 16     if(strcmp(argv[1], "a") == 0)       //需要用strcmp对两个值进行判断,而不能用==直接判断
 17     {
 18         printf("snowdragon receive a!\n");
 19     }
 20     else if(strcmp(argv[1], "b") == 0)
 21     {
 22         printf("snowdragon receive b!\n");
 23     }
 24     else if(strcmp(argv[1], "c") == 0)
 25     {
 26         printf("snowdragon receive c!\n");
 27     }
 28     else
 29     {
 30         printf("default\n");
 31     }
 32 
 33     return 0;
 34 }

5、运行结果

在这里插入图片描述

本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐