Linux进程控制

进程创建

fork创建子进程

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

#include <unistd.h>

pid_t fork(void);

返回值:自进程中返回0,父进程返回子进程id,出错返回-1

fork创建子进程,操作系统做了什么操作?

  • 分配新的内存块和内核数据结构给子进程

  • 将父进程部分数据结构内容拷贝至子进程

  • 添加子进程到系统进程列表当中

  • fork返回,开始调度器调度

在这里插入图片描述

fork函数的返回值

fork函数为什么要给子进程返回0,给父进程返回子进程的PID?

一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。

因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。

fork函数为什么有两个返回值?

在这里插入图片描述

写时拷贝

当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

在这里插入图片描述

OS为何要采用写时拷贝将父子进程分离?

在这里插入图片描述

fork调用失败的原因

  • 系统中有太多的进程(内存不够

  • 实际用户的进程数超过了限制(用户是有创建进程数量限制的

进程终止

退出码

在这里插入图片描述

main函数中return的理解

main函数是间接性被操作系统所调用的,当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,

我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。
在这里插入图片描述
在这里插入图片描述

exit()与_exit()

exit函数

#include <unistd.h>

void exit(int status);

exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:

  1. 执行用户通过atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit函数终止进程。

_exit函数

_ exit函数用法与exit类似,_ exit函数退出进程的方法我们并不经常使用,_ exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。

exit()与_exit()函数的区别

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

在这里插入图片描述

进程异常退出

情况一:向进程发生信号导致进程异常退出。

例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。

情况二:代码错误导致进程运行时异常退出。

例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。

进程等待

进程等待的必要性

  1. 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏
  2. 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程
  3. 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
  4. 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。

进程等待方法

wait方法

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int*status);

返回值:
​ 成功返回被等待进程pid,失败返回-1。

参数:

​ 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
在这里插入图片描述

waitpid方法

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

返回值:

​ 当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

pid

Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。

status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

options:

默认为0,表示阻塞等待

WNOHANG: ,非阻塞等待,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。

若正常结束,则返回该子进程的ID。
在这里插入图片描述

获取子进程的status

在这里插入图片描述

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

waitpid被调用时内部伪代码模型

在这里插入图片描述

父进程要拿到子进程的退出信息,为什么要用wait/waitpid函数?为什么不用全局变量?

父子进程虽然共用同一份代码,但当某个变量要修改时,就会发生写时拷贝,

又因为进程具有独立性,父进程无法获取子进程已经修改的变量的值。

既然进程具有独立性,进程退出码也是子进程的数据,父进程是如何拿到的呢?wait/wawitpid做了什么工作呢

僵尸进程是一个死亡进程,代码和数据都会被释放,但该进程的PCB还保留着,task_struct保存了该进程退出时的退出信息。

进程退出时,进程的退出码和退出信号就会被写入到PCB中。

在这里插入图片描述

基于非阻塞接口的轮询检测方案

当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。

实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。

在这里插入图片描述

进程替换

替换原理

用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
在这里插入图片描述

在这里插入图片描述

当进行进程程序替换时,有没有创建新的进程?

进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。

子进程进行进程程序替换后,会影响父进程的代码和数据吗?

子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

替换函数

execl()

在这里插入图片描述

不创建子进程的程序替换

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

创建子进程的程序替换

在这里插入图片描述

execv()

在这里插入图片描述

execlp()

在这里插入图片描述

execvp()

在这里插入图片描述

execle()

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

makefile中如何同时编译多个项目

在这里插入图片描述

注意

以上几个程序替换函数都是系统提供的基本封装,他们底层都是调用execve这个系统直接提供的进程替换接口。

系统直接提供的进程替换接口:

**int execve(const char *filename, char const argv[], char const envp[]);
在这里插入图片描述

做一个简易的shell

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
     
#define NUM 1024
#define SIZE 32
#define SEP " "
    
//保存完整的命令字符串
char cmd_line[NUM];
//保存切割后的命令行字符串
char *g_argv[SIZE];
// 保存从g_argv拷贝来的字符串
char my_argv[64];
    
//shell运行原理:通过让子进程执行命令,父进程等待&&解析命令
int main()
{
     //0.命令行解释器,一定是一个常驻内存的进程,不退出
     while(1)                                                    
     {
        //1.打印出提示信息 [lhj@localhost shell]#
        printf("[lhj@localhost shell]# ");
        fflush(stdout);//刷新缓冲区
        memset(cmd_line,'\0',sizeof cmd_line);
		//2.获取用户的键盘输入[输入的是各种指令和选项 :"ls -a -l"]
        if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)                                                
        {
            continue; 
        }
        //将用户输入的回车键设置为\0 使用户输入的指令与提示信息在同一行
        //例如:当用户输入 ls -a -l 然后回车 ,缓冲区读到的数据为:"ls -a -l \n\0"
        //为了防止换行,就得把\n替换成\0
        cmd_line[strlen(cmd_line)-1]='\0';
        //3.解析命令行字符串,将用户输入的字符串分割成子字符串
        //例如:用户输入"ls -a -l" -> "ls" "-a" "-l"
        g_argv[0]=strtok(cmd_line,SEP);//切割原始字符串,第一次调用,要传入原始字符串
        int index=1;
        if(strcmp(g_argv[0],"ls")==0)
        {
            g_argv[index++]="--color=auto";//上颜色
        }
        if(strcmp(g_argv[0],"ll")==0)
        {
             g_argv[0]="ls";
             g_argv[index++]="-l";
             g_argv[index++]="--color=auto";
        }
		//用循环将整个字符串都全部切割完
        //第二次若是还要切割原始字符串,可以直接传入NULL
        while(g_argv[index++]=strtok(NULL,SEP));
        if(strcmp(g_argv[0],"export")==0&&g_argv[1]!=NULL)
        {
            // cmd_line 中保存的是完整的字符串
            // g_argv中保存的是cmd中的子字符串
            //g_argv指针数组中保存的是字符串在cmd_line中的地址
            //当下一次执行循环时,cmd_line被清空后,环境变量的地址没变
            //但是cmd_line的被清空了,故需要将g_argv的内容拷贝到my_argv中  
            strcpy(my_argv,g_argv[1]);                                                       
            int ret=putenv(my_argv);
            if(ret==0)
                printf("%s export success\n",g_argv[1]);
        }
        //4.内置命令,让父进程(shell)自己执行的命令,我们叫做内置命令
        //内置命令本质就是shell中的一个函数调用
        if(strcmp(g_argv[0],"cd")==0)
        {
            if(g_argv[1]!=NULL) 
                 chdir(g_argv[1]);
            continue;
        }
        //5.fork()
        pid_t id=fork();
        if(id==0)//child
        {
            printf("下面功能是让子进程进行的\n");
            execvp(g_argv[0],g_argv);
            exit(1);
        }
         //father
         int status=0;
         pid_t ret=waitpid(id,&status,0);
            if(ret >0)
 		 		printf("exit code:%d\n",WEXITSTATUS(status));
      }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值