进程控制学习

学习目标

掌握进程创建

掌握到进程等待

掌握到进程程序替换

模拟实现微型shell,深入认识shell运行原理

掌握到进程终止,认识$?

进程创建

1、fork函数

1)fork声明
#include <unistd.h>
pid_t fork(void);

注意:

在父进程中,fork函数返回子进程的PID。

在子进程中,fork函数返回0。

如果创建子进程失败,fork函数返回-1。

2)fork函数的简单使用
int main() {
    pid_t pid = fork();
    while(1)
    {
        if (pid == -1) {
            // 创建子进程失败(可能由于过多的进程导致)
            perror("fork failed");
            return 1;
        } else if (pid == 0) {
            // 子进程逻辑
            printf("This is the child process.\n");
        } else {
            // 父进程逻辑
            printf("This is the parent process. Child PID: %d\n", pid);
        }
    }
    return 0;
}

简要说明:

子进程继承父进程,代码和数据是共享的,但是它们各自独立的执行程序。

子进程从fork函数调用的位置开始执行,继续执行之后的代码。

通过检查返回值,父进程可以判断当前进程是否是子进程,从而分别执行不同的逻辑。

父进程和子进程是并发执行的,并且它们的执行顺序是不确定的,取决于操作系统的调度算法和优先级策略

3)深入理解写时拷贝

父子代码和数据是共享的,但对物理内存发生修改访问(写入操作、程序替换)时,子进程会在内存产生对应资源,同时也修改了页表的只读属性,保证了进程的独立性,减少了资源冗余。

2、内核对进程创建的管理

给子进程分配新的内存块和内核数据结构(task_struct、mm_struct、页表)

将父进程部分数据结构(环境变量、时间片、epi数据)拷贝至子进程,通过写时拷贝,管理数据和代码的独立或贡献

  • eip寄存器:程序计数器,保存当前正在执行指令的下一条指令的地址

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

返回后,开始调度器调度

进程终止

进程终止是指一个正在运行的进程停止执行并结束其生命周期,它会向其父进程发送一个退出状态码,以便父进程可以根据该状态码判断子进程的执行结果,进而执行一系列的清理工作,包括关闭文件、释放内存等。

1、进程终止方式

1)正常终止

程序执行完毕

  • main函数return

return对象:父进程

return 0:正常终止,结果正确

return 非0:正常终止,结果错误

  • exit、_exit

2)异常终止
  • 信号终止

kill -l:可以列出系统支持的所有信号及其对应的编号

kill -信号编号 pid:可以给进程发送信号

常见的终止信号:

  1. SIGTERM(15):该信号是默认的终止信号,用于请求进程正常终止。

  2. SIGKILL(9):该信号是不可捕获的终止信号,用于强制终止进程。

  3. SIGINT(2):该信号是中断信号,通常由用户在终端上按下Ctrl+C时发送给前台进程组,用于中断正在运行的进程。

  4. SIGQUIT(3):该信号是退出信号,通常由用户在终端上按下Ctrl+\时发送给前台进程组,用于请求进程终止,并生成核心转储文件。

  5. SIGSTOP(19):该信号用于暂停进程的执行。与SIGKILL不同,被该信号停止的进程可以通过发送SIGCONT信号恢复执行。

  • 错误终止

2、exit函数

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

注意:

整数参数status 是退出状态码,父进程通过进程等待来获取子进程退出状态,了解子进程的执行结果。

exit函数是C语言库中的一个函数,它会在终止进程之前执行一些清理操作,例如关闭文件、刷新缓冲区等。

2)exit简单使用
int main() {
    // 使用exit函数正常终止进程,并返回状态码
    printf("Exiting with status code 0");
    exit(0);        //echo $?:显示在bash最近一次进程的退出码
    // 使用_exit函数立即终止进程,不会执行该行以后的代码
    printf("This line will not be executed");
}
3)_exit

_exit函数属于系统调用,用于立即终止进程,不执行任何清理操作。例如关闭文件、刷新缓冲区。

3、内核对进程终止的管理

进程终止后,内核维护了进程的状态信息,一般将进程设置为Z状态,为其父进程返回退出信息状态,清理与进程相关的资源,但可能会对进程PCB数据结构有所保留,方便下次调用

进程等待

进程等待是指一个进程等待另一个进程的终止。父进程常常需要等待子进程的执行完成,以便获取子进程的执行结果或进行进一步处理。等待进程终止可以使用wait或waitpid系统调用,它们会使父进程阻塞,直到指定的子进程终止。在等待过程中,父进程可以获取子进程的退出状态,以便进行后续处理

1、进程等待函数

1)wait
  • 声明

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

参数status是一个指向整数的指针,输出型参数,用于存储子进程的终止状态。

函数的返回值是子进程的PID,可以通过返回值来判断等待的子进程是哪个进程终止。

如果返回值为-1,表示发生了错误。

  • status

status参数用于获取子进程的退出状态,但不仅限于是整数

  • 高8位表示子进程退出的状态值,如果该子进程是由于接收到信号而退出的,这个值就是0;可以用(status>>8)&0xFF获取

  • 低7位表示导致子进程退出的信号号码,如果该子进程是正常退出的,这个值就是0;可以用(status)&0x7F获取

  • 最低位(第7位)如果是1,表示产生core dump文件,否则没有。

status可以被以下几个宏用来检测子进程的退出状态:

  • WIFEXITED(status):如果子进程正常结束则返回真;

  • WEXITSTATUS(status):返回子进程的退出状态,只有在WIFEXITED返回真时才有意义;

  • WIFSIGNALED(status):如果子进程是由于接收到信号而结束则返回真;

  • WTERMSIG(status):返回导致子进程终止的信号的编号,只有在WIFSIGNALED返回真时才有意义;

  • WIFSTOPPED(status):如果子进程被停止则返回真;

  • WSTOPSIG(status):返回引起子进程暂停的信号的编号,只有在WIFSTOPPED返回真时才有意义;

  • WIFCONTINUED(status):如果子进程被继续运行则返回真

  • 简单使用

int main()
{
    pid_t id=fork();
    if(id==0){//子进程
        printf("This is child process,pid=%d\n",getpid());
        sleep(5);
        exit(123);//正常终止
    }
    //此时一定是父进程
    printf("This is parent process,pid=%d\n",getpid());
    printf("Waitting child process to exit\n");
    int status=0;
    pid_t ret=wait(&status);//输出型参数
    if(ret){
        printf("Waitting sucessed,the exit signal is %d,code is %d\n",status&0x7F,(status>>8)&0xFF);
        //正常终止时的终止状态或者信号终止时的信号
    }
    // if(WIFEXITED(status)){      //采用宏定义判断
            //printf("Waitting sucessed,code is %d\n",WEXITSTATUS(status));
}

上述过程属于父进程属于阻塞等待

  • 在阻塞等待中,进程会暂停执行,直到被等待的事件发生或操作完成。

  • 阻塞等待通常会导致进程的状态变为阻塞状态,进程会等待在某个特定的等待队列中,直到满足等待条件。

  • 当一个进程执行阻塞等待时,它会释放CPU的控制权,将CPU资源让给其他可执行的进程。

  • 一旦被等待的事件发生或操作完成,进程会被唤醒并继续执行。

2)waitpid
  • 声明

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)

pid:指定要等待的进程的PID。可以传入以下值:

  • -1:等待任意子进程。

  • 0:等待与调用进程属于同一个进程组的任意子进程。

  • 正整数:等待指定PID的子进程。

options:指定等待的选项。常用的选项包括:

  • 0:默认选项,表示阻塞等待子进程的状态变化。

  • WNOHANG:表示非阻塞等待子进程的状态变化

返回值:

当正常终止的时候,返回子进程的pid;

option设置为WNOHANG时,子进程还没有退出则返回0;

发生cuo'wu,则返回-1;

  • 简单使用

int main()
{
    pid_t id=fork();
    if(id==0){//子进程
        printf("This is child process\n");
        sleep(20);
        exit(123);//正常终止
    }
    //此时一定是父进程
    printf("This is parent process\n");
    int status=0;
    //轮询检查
    while(1)
    {
        printf("Waitting child process to exit\n");
        pid_t ret=waitpid(-1,&status,WNOHANG);//非阻塞等待
        if(ret==0){
            printf("The parent process do things\n");
        }
        else if(ret==id){
            printf("Waitting sucessed,code is %d\n",WEXITSTATUS(status));
            //正常终止时的终止状态
            break;
        }
        else{
            printf("非正常返回\n");//信号终止或者发生错误
        }
        sleep(1);
    }
}

上述过程属于父进程属于非阻塞等待

  • 在非阻塞等待中,进程会以轮询的方式检查被等待的事件是否发生或操作是否完成。

  • 一个进程在非阻塞等待(busy-waiting或轮询)时,其状态通常被视为运行态(Running)

  • 进程会持续地检查等待条件,而不会暂停执行或释放CPU控制权。

  • 如果等待条件尚未满足,进程可以执行其他操作或逻辑,不必一直等待。

2、内核对进程等待的管理

内核在进程阻塞等待期间会将进程置于阻塞状态,并将其从可执行队列中移除,以避免浪费CPU资源。当等待条件满足时,内核会唤醒被阻塞的进程,使其重新开始执行。等待过程中,内核会周期性地检查等待条件是否满足,以确保进程可以及时被唤醒。

进程程序替换

一个正在运行的进程中将当前执行的程序替换为另一个程序的操作,替换原理是先找到执行程序,再选择执行方式。子程序的替换不会影响父进程

1、exec函数

1)声明
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);//系统调用

注意:

返回值:函数调用成功,不再返回,调用出错则返回-1。

参数

  • path/file:指向要执行的程序文件的路径字符串,该路径可以是绝对路径也可以是相对路径。该程序文件必须是一个可执行文件,即具有可执行权限的文件。同时,有些情况也可以直接使用在环境变量PATH下的可执行的程序文件。

  • argv:指向一个以NULL结尾的字符串数组,表示要传递给新程序的命令行参数。数组中的第一个元素通常是新程序的名称,随后是其他参数选项。注意,argv参数应该是一个指向字符指针的指针,即char *const argv[]

命名规则

  • l(list) : 表示使用参数列表

  • v(vector) : 表示用数组

  • p(path) : 表示自动搜索环境变量PATH

  • e(env) : 表示自己维护环境变量

exec函数众多,但只有execve是系统调用,其他函数是基于此函数的封装

2)简单使用
int main()
{ 
    pid_t id fork();
    if(id==0)
    {
        printf("This is child process\n");
        char *const env_[] = {(char*)"MYPATH=YouCanSeeMe!!",NULL};//设置继承的环境变量
        printf("Prepare the replacement program\n");
        execel("./myproc","myproc",NULL, env_);//环境变量的添加时覆盖式的
        exit(-1);//只要执行这一行,说明替换失败
    }
}

exec函数有多种,根据不同的条件,选择合适的函数

2、内核对进程程序替换的管理

当进程请求替换当前程序时,内核会负责加载新的可执行文件到进程的内存中,更新页表,进行上下文切换,分配其他必要的资源。

内核会处理程序替换过程中可能出现的错误。

在程序替换完成后,内核会清理旧的页表,释放相关的资源

3、模拟实现shell

#include <stdio.h>
#include <string.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SEP " "
#define NUM 1024
#define SIZE 128
char command_line[NUM];
char *command_args[SIZE];
char env_buffer[NUM]; //for test
extern char**environ;
//对应上层的内建命令--不创建进程,而是调用函数
int ChangeDir(const char * new_path)
{
    chdir(new_path);//
    return 0; // 调用成功
}
void PutEnvInMyShell(char * new_env)
{
    putenv(new_env);
}
int main()
{
    while(1)
    {
        //1. 显示提示符
        printf("[输入指令]:");
        fflush(stdout);//不换行刷新缓冲区
        //2. 获取用户输入
        memset(command_line, '\0', sizeof(command_line));
        fgets(command_line, sizeof(command_line, stdin); //标准输入stdin, 不要忽视'\n'
        command_line[strlen(command_line) - 1] = '\0';// 清空\n
        //3.字符串切分
        command_args[0] = strtok(command_line, SEP);//strtok字符串分割的函数
        int index = 1;
        // 给ls命令添加颜色
        if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) 
            command_args[index++] = (char*)"--color=auto";
        // strtok 截取成功,返回字符串其实地址
        // 截取失败,返回NULL
        while(command_args[index++] = strtok(NULL, SEP));//以字符串切分
        // 4.内建命令cd、export
        if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            ChangeDir(command_args[1]); //让调用方进行路径切换
            continue;
        }
        if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
        {
            // 目前,环境变量信息在command_line,会被清空
            // 此处我们需要自己保存一下环境变量内容
            strcpy(env_buffer, command_args[1]);
            PutEnvInMyShell(env_buffer); //注意环境变量输入格式
            continue;
        }
        // 5. 创建进程,执行
        pid_t id = fork();
        if(id == 0)
        {//child
            // 6. 程序替换
            execvp(command_args[0], command_args);
            exit(1); //执行到这里,子进程替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值