【Linux】进程控制


目录

1、进程创建

1.1、fork函数内部完成的功能

1.2、用户空间&内核空间

1.3、写时拷贝

2、进程终止

2.1、进程终止的场景 

2.2正常终止

 2.3、异常终止

2.4、exit和_exit函数的区别

1、执行用户自定义的清理函数

 2、冲刷缓冲区,关闭流等

 2.5、缓冲区

冲刷缓冲区的方式

缓冲方式

3、进程等待

3.1:为什么要进程等待

3,2、进程等待的方法

wait方法:

waitpid函数:

参数status的含义:

代码验证

1、wait函数

 2、waitpid函数

4、进程程序替换

4.1、为什么要有进程程序替换

 4.2、替换原理

4.3、exec函数簇

1、execl函数

 3、execle函数

4、execv函数

 5、execvp函数

6、execve函数

 拓展



1、进程创建

1.1、fork函数内部完成的功能

创建子进程,子进程拷贝父进程的PCB。

①分配新的内存和内核数据结构(task_struct)给子进程。

②将父进程部分数据结构拷贝至子进程。

③添加子进程到系统列表中,添加到双向链表中。

④ork返回,开始调度器调度(操作系统开始调度)

1.2、用户空间&内核空间

内核空间 :

Linux操作系统和驱动程序运行在内核空间。系统调用函数都是在内核空间运行的,因为是操作系统通过的函数。

用户空间:

应用程序都是运行在用户空间的。若程序员写的代码调用了系统调用函数,则会切换到内核空间执行,系统调用函数执行完毕后,再返回到用户空间继续执行用户代码。

1.3、写时拷贝

父进程 创建出来子进程,子进程的PCB拷贝父进程,页表也是拷贝父进程的。起初,操作系统并没有给子进程当中的变量分配空间进行存储,子进程当中的变量还是原来父进程物理地址当中的内容。如果不改变变量值,父子进程共享一个数据。如果改变变量值,才以写实拷贝的方式拷贝一份。此时父子进程通过各自的页表,指向不同的物理地址。

图解如下:

2、进程终止

2.1、进程终止的场景 

场景一:代码运行结束,结果正确

场景二:代码运行结束,结果不正确。

场景三:代码异常终止

2.2正常终止

可以通过echo $?来查看进程的退出码。

①从main函数的ruturn(并不是任何函数的return语句都可以结束进程,必须是mian函数的return语句)

②调用exit函数(c标准库函数)

#include<stdlib.h>

void exit(int status);

参数:进程退出时的退出码。   作用:谁调用终止谁。     

③调用_exit函数(系统调用函数)

#include<unistd.h>

void _exit(int status) 

 2.3、异常终止

程序崩溃(内存访问越界,访问空指针)

Ctrl + c命令

2.4、exit和_exit函数的区别

exit函数比_exit函数多执行了两个步骤:

1、执行用户自定义的清理函数

#include<stdlib.h>

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

功能:注册一个函数,在进程终止的时候调用,被调用的函数只能是返回值类型为void的无参函数

 

 2、冲刷缓冲区,关闭流等

缓冲区:c标准库定义的,而非内核。

建立缓冲区的目的:减少IO次数,以内IO操作比较耗费时间。

当触发刷新缓冲区的条件后,缓冲区的内容才会继续进行IO操作

关闭流:标准输入、标准输出、标准错误

  

 2.5、缓冲区

冲刷缓冲区的方式

①exit()   ②main函数中的return  ③fflush  ④\n

缓冲方式

全缓冲:当缓冲区写满了才进行IO

行缓冲:当在输入输出中遇到换行符的时候

不缓冲:不带缓冲,标准IO库不对字符进行缓冲存储

3、进程等待

3.1:为什么要进程等待

已知子进程先于父进程退出,父进程如果不管不顾,子进程就会变成僵尸进程,进而造成内存泄漏。   进程一旦进入僵尸状态,就会刀枪不入,“杀人狂魔”的kill - 9也无能为力,因为谁也没有办法杀死一个死去的进程。        但是,父进程给子进程的任务他完成的如何,我们需要知道。        父进程通过进程等待的方式,回收子进程资源,获取子进程退出状态信息。

总之,父进程进行进程等待,等待子进程退出之后。回收子进程的退出状态信息,防止子进程变成僵尸进程。

3,2、进程等待的方法

wait方法:

#include<sys/wait.h>

#include<sys/types.h>

pid_t wait(int* status);

返回值:返回被等待进程的pid,失败则返回-1

函数参数:输出型参数,获取子进程状态,不关心则可以设置为 NULL

函数特性:

阻塞的:谁调用,谁等待。直到等待的子程序退出。

两种情况:

①发起阻塞,资源存在,无需等待,直接执行函数功能后返回。

②发起阻塞,资源不在,等待资源到来后,执行函数功能返回。

waitpid函数:

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

返回值:返回收集到的pid。设置了WNOHANG选项,调用过程中waitpid发现没有已退出的子程序可以收集,则返回0;如果调用出错,返回-1,这时errno会被设置为相应的值以指示错误所在。

pid:pid = -1,等待任意一个子进程,与wait等效;pid > 0,等待进程id与pid相等的子程序。

status:后面总结,这两个函数的status含义一样。

options:WNOHANG:若pid指定的进程没有结束,则waitpid函数返回0;不予等待(并没有完成函数功能,若正常结束,则返回该子进程的pid)

函数特性:

参数options被设置为WNOHANG后,为非阻塞。

非阻塞:当调用一个非阻塞的函数时候,函数会判断资源是否准备好。准备好:执行函数功能返回;没准备好:函数报错返回(注:函数功能并没有完成)

要点:非阻塞要搭配循环来使用。

参数status的含义:

 wait和waitpid都有一个参数status,该参数为输出型参数,由操作系统填充;如果传递NULL,表示不关心子进程退出状态信息;否则,操作系统会修改该参数,将子进程的退出信息返回给父进程;status不能简单的当作整形来看,我们只有四字节的低两个字节。

如何判断子进程正常退出还是异常退出?

正常退出:返回值>0&&退出信号没有被设置(==0)

异常退出:返回值>0&&退出信号被设置(>0)

代码验证

1、wait函数

①子进程还是僵尸进程吗?

 1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/wait.h>
  4 #include<stdlib.h>
  5 int main(){
  6     pid_t ret = fork();
  7     if(ret == -1){
  8         return -1;
  9     }else if(ret == 0){
 10         //child
 11         printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
 12     }else{
 13         //father
 14         printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
 15         wait(NULL);                                                                                                                               
 16         while(1){
 17             sleep(1);
 18         }
 19     }

②正常情况下获取status值

1 #include<stdio.h>
  2 #include<uni  d.h>                                                                                                                                                
  3 #include<sys/wait.h>
  4 #include<stdlib.h>
  5 int main(){
  6     pid_t ret = fork();
  7     if(ret == -1){
  8         return -1;
  9     }else if(ret == 0){
 10         //child
 11         printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
 12         exit(99);
 13     }else{
 14         //father
 15         printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
 16         int status = 0;
 17         int ret = wait(&status);
 18         if(ret == -1){
 19             return -1;
 20         }else if (ret > 0 && ((status & 0x7f) == 0)){
 21             //子进程正常退出
 22             printf("child process return code is %d\n",(status >> 8)&0xff);
 23         }else{
 24             //异常退出
 25             printf("child process receive signal is %d,coredump flag is %d\n",status&0x7f,(status>>7)&0x1);
 26         }
 27     }
 28     return 0;
 29 }

 ③异常退出 

为什么这里的coredump标志位还是0?

原因为没有设置coredump文件。

 修改: 

 2、waitpid函数

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/wait.h>
  4 #include<stdlib.h>
  5 int main(){
  6     pid_t ret = fork();
  7     if(ret == -1){
  8         return -1;
  9     }else if(ret == 0){
 10         //child
 11         printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
 12         exit(99);
 13     }else{
 14         //father pid:这个含义就是刚刚创建出来的子进程的PID                                                                                        
 15         printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
 16         waitpid(ret,NULL,WNOHANG);
 17         while(1){
 18             sleep(1);
 19         }
 20     }

 

 可以看到子进程是一个僵尸进程。原因是非阻塞要搭配循环来使用。

1 #include<stdio.h>
  2 #include<unistd.h>
  3   nclude<sys/wait.h>                                                                                                                                              
  4 #include<stdlib.h>
  5 int main(){
  6     pid_t ret = fork();
  7     if(ret == -1){
  8         return -1;
  9     }else if(ret == 0){
 10         //child
 11         printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
 12         exit(99);
 13     }else{
 14         //father pid:这个含义就是刚刚创建出来的子进程的PID
 15         printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
 16         int re  = 0;
 17         do{
 18              re = waitpid(ret,NULL,WNOHANG);
 19         }while(re == 0);
 20         while(1){
 21             sleep(1);
 22         }
 23     }
 24     return 0;
 25 }

  

 

 可以看到搭配循环后子进程等待到了子进程。

4、进程程序替换

4.1、为什么要有进程程序替换

想让程序执行不一样的代码。用为父进程创建出来的子进程和父进程拥有相同的代码段,所以,子进程看到的代码和父进程是一样的。当我们想要让子进程去执行不同的代码段的时候,就需要让子进程调用进程程序替换的接口,从而让子程序执行不一样的代码。

 4.2、替换原理

fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数 以执行另一个程序。当进程调用一种exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动 例程开始执行。调用exec 并不创建新进程 , 所以调用 exec 前后该进程的 id 并未改变。

 替换进程的数据段和代码段并更新堆栈。


4.3、exec函数簇

这些函数包含的头文件都为<unistg,h>

返回值:如果调用成功,加载新的程序从启动代码(main)开始执行,不再返回;如果调用失败,则返回-1.

1、execl函数

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

path:带路径的可执行程序(需要路径)。

arg:传递给可执行程序的命令行参数,第一个参数必须是可执行程序本身。如果要传递多个参数,则用‘,’将其隔开,最后用NULL结尾。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 int main(){
  4     printf("i am main..., process start...\n");
  5     int ret = execl("/bin/pwd", "pwd", NULL);
  6     printf("if run here, execl failed : %d\n", ret);
  7     return 0;
  8 }                                                                                                                                                
~

2、execlp函数

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

file:可执行程序,可以不带路径,也可以带路径。execlp函数会去搜索PATH这个环境变量。若可执行程序在PATH中,则正常替换,执行替换后的程序;若不在,则报错返回,替换失败。


  1 #include <stdio.h>
  2 #include <unistd.h>
  3 int main(){
  4     printf("i am main..., process start...\n");
  5     int ret = execlp("pwd", "pwd", NULL);
  6     printf("if run here, execl failed : %d\n", ret);
  7     return 0;                                                                                                                                    
  8 }

 3、execle函数

 int execle(const char *path, const char *arg,   ..., char * const envp[]);
参数:相交于execl,增加了一个envp[],剩下的完全一致。

envp:程序员传递的环境变量,程序员在调用该函数的时候,需要自己组织环境变量传递给函数。

1 #include <stdio.h>
  2 #include <unistd.h>
  3 int main(){
  4     printf("i am main..., process start...\n");
  5     extern char** environ;
  6     int ret = execle("/home/DL/linux/process/pro_exec/execle/mygetenv/mygetenv", "mygetenv", NULL, environ);                                      
  7     printf("if run here, execlp failed : %d\n", ret);
  8     return 0;
  9 }
#include <stdio.h>
#include <stdlib.h>
int main(){
    printf("%s:%d\n", __FILE__, __LINE__);
    printf("%s\n", getenv("PATH"));
    return 0;
}

4、execv函数

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

 argv:也是传递给可执行程序的命令行参数,但是必须以指针数组的方式进行传递。剩下的与execl一样。

#include <stdio.h>
#include <unistd.h>
int main(){
    printf("i am main..., process start...\n");
    char* argv[10] = {NULL};
    argv[0] ="ls";
    argv[1] = "-l";
    int ret = execv("/bin/ls", argv);
    printf("if run here, execl failed : %d\n", ret);
    return 0;
}

 

 5、execvp函数

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

argv必须以指针数组的方式传递,其余与enecvp一样

6、execve函数

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

除了要以针织数组的方式传数据,其余与execle一样。

 拓展

函数之间的关系和区别:

execve是系统调用函数,其他5个都是库函数 

 

 

 

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
/* 
 * * 1.创建子进程 
 * * 2.父子进程执行不同的逻辑 
 * *     子进程进行进程程序替换 
 * *     父进程进行等待 
 * * */
int main(){
    pid_t pid = fork();
    if(pid < 0){
        printf("fork failed\n");
        return 0;
    }else if(pid == 0)
    {        
        //child
        printf("i am child, start exec...\n");
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        printf("if you see the msg, exec failed\n");
    }else{
        //father        
        printf("i am father, i prepare wait child process\n");
        wait(NULL);
    }
    return 0;
}

 

 埋点:父进程是死等子进程的,在子进程不退出之前,父进程是什么事情都不能干的。

问:有没有可能让父进程在执行自己代码的情况下,当子进程退出了,父进程还能及时调用wait进行等待。

答:当前的知识储备是解决不了的,等到学过进程信号之后,就有办法来解决了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值