当前位置: 首页 > news >正文

【Linux系统编程】进程控制

【Linux系统编程】进程控制

  • 1. 进程创建
    • 1.1 fork函数
    • 1.2 创建多个进程
    • 1.3 写时拷贝
  • 2. 进程终止
    • 2.1 进程退出场景
    • 2.2 进程退出方法
  • 3. 进程等待
    • 3.1 进程等待的必要性
    • 3.2 进程等待的方法
      • 3.2.1 wait方法
      • 3.2.2 waitpid方法
    • 3.3 获取子进程status
    • 3.4 阻塞等待与非阻塞等待
  • 4. 进程程序替换
    • 4.1 替换原理
    • 4.2 替换函数
      • 4.2.1 函数解释
      • 4.2.2 命名理解
  • 5. 自主Shell命令行解释器
    • 5.1 目标
    • 5.2 实现原理
    • 5.3 源码

1. 进程创建

1.1 fork函数

在Linux中fork函数是非常重要的函数,它从已存在的进程中创建一个新进程,新进程为子进程,而原进程为父进程,

关于fork函数的使用和返回值我已经在【Linux系统编程】进程概念(二)进程的概念和基本操作 中 2.4 如何创建子进程已经说过了,但是当时还在初识阶段,对其描述不够多,这里我就用文字再次说明一下。

以下是调用 fork() 函数后,操作系统内核和执行流所发生的关键变化:

  1. 创建子进程内核结构
    当程序调用 fork() 后,操作系统会执行一系列操作来创建一个新的子进程。这首先包括为子进程分配独立的内核数据结构,其中最关键的是进程控制块(PCB,在 Linux 中通常指 task_struct),它用于标识和管理进程。同时,内核也会为子进程创建自己的进程地址空间描述符(如 mm_struct)和页表 。

  2. 复制父进程上下文
    在创建好基本的内核结构后,操作系统会将父进程的 task_structmm_struct 等数据结构中的大部分字段内容复制给子进程对应的结构。这里一个重要的优化是 写时拷贝(Copy-On-Write, COW)。内核最初会让父子进程的页表项指向相同的物理内存页,并将这些页面标记为只读。只有当任一进程试图写入数据时,才会触发页错误,进而为该进程复制一个独立的页面副本,从而实现修改隔离,这有效减少了进程创建的开销和内存占用 。

  3. 将子进程加入调度队列
    完成资源复制和初始化后,子进程会被设置为可运行状态(TASK_RUNNING),并由 wake_up_new_task 函数将其添加到当前 CPU 或其他可用 CPU 的运行队列中,等待操作系统的调度 。

  4. fork 返回与调度器介入
    至此,fork() 调用准备返回。一个关键特性是,fork() 会返回两次:一次在父进程上下文中,一次在子进程上下文中。在父进程中,其返回值是新创建子进程的进程 ID(PID);在子进程中,返回值是 0 。从这一刻起,父进程和子进程成为两个独立的执行流,它们谁先被调度到 CPU 上运行是不确定的,这完全由操作系统的调度算法决定 。

父子进程的执行逻辑

  • 共用代码与执行流分离fork() 调用之后的代码,父子进程都拥有一份相同的副本。如果不加任何控制,两个进程都会从 fork() 返回处开始继续执行后续的所有代码。
  • 利用返回值分流:为了让父子进程执行不同的任务,需要通过判断 fork() 的返回值来引导它们进入不同的代码分支。通常使用 if (pid == 0) 来识别子进程,并让其执行特定的逻辑;父进程则进入另一个分支 。

处理 fork 失败

fork() 调用有可能失败,此时它返回 -1。失败的主要原因通常包括:

  • 系统进程总数或单个用户可创建的进程数已达到上限 。
  • 系统内存不足,无法为子进程分配必要的内核数据结构 。
    虽然在很多简单程序中可能不做详细错误处理,但在健壮性要求高的软件中,检查 fork 的返回值并处理错误是必要的。

1.2 创建多个进程

我们可以使用for循环创建多个进程,在循环中创建多个子进程的核心在于:确保只有原始的父进程能够连续执行循环并创建所有子进程,而每个新创建的子进程在执行完特定任务后必须立即退出循环,防止其继续创建“孙子”进程,从而避免进程数量的指数级爆炸 , 所以这里会用到exit()函数,等子进程任务完成后直接终止子进程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{int i = 0;for(; i < 5; ++i){pid_t id = fork();if(id == 0){// childint cnt = 5;while(cnt--){                                                                                                                                                                     printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);}exit(0);}}sleep(100);return 0;
}
  1. 循环与进程创建
    父进程进入一个循环(例如5次)。每次迭代中,它调用 fork() 系统调用。fork() 会复制当前进程(父进程),创建一个几乎完全相同的子进程 。

  2. 利用返回值区分执行流
    fork() 的关键特性是调用一次,返回两次:在父进程中返回新创建子进程的PID(大于0的正整数),在子进程中返回0。通过判断返回值,可以明确地区分当前代码是在父进程还是子进程中执行 。创建失败时,fork() 返回-1,这里只是看看能否创建多个进程,所以就不考虑失败的情况。

  3. 子进程的执行与退出
    当检测到 fork() 返回值为0时,表示当前处于子进程。子进程应执行其专属任务。最关键的一步是,子进程在任务完成后必须立即调用 exit()(或 _exit())终止 。这确保了子进程不会继续执行循环体内的后续代码,特别是不会再次调用 fork(),从而避免了进程的链式或指数级创建 。

  4. 父进程的继续循环
    父进程获得子进程的PID(返回值大于0),因此不会进入子进程的代码分支。它会继续执行下一次循环,再次调用 fork() 创建下一个子进程 。

  5. 并发执行与观察
    由于现代操作系统的进程调度和fork的执行速度,父进程连续创建多个子进程的过程非常快,这些子进程几乎是同时被创建并进入就绪状态的。调度器会决定它们的实际运行顺序,因此它们实际上是并发执行的 。通过 ps 命令在另一个终端窗口实时监视进程状态,是验证多进程创建和并发执行的有效方法。

1.3 写时拷贝

通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式生成一份副本,具体看下图。

在这里插入图片描述

下面,我们就来深入剖析一下操作系统是如何像一位“幕后导演”一样,精准感知“修改”动作并触发写时拷贝这出“大戏”的。

操作系统的感知与触发机制

  1. 设置“陷阱”:权限标记为只读
    当父进程调用 fork() 创建子进程时,操作系统并不会立即复制整个地址空间的数据。相反,它会让父子进程共享同一份物理内存页。为了能监控之后的写入行为,操作系统会将所有共享的物理内存页(存放数据、堆栈等)在父子进程的页表项中的权限标记为“只读” 。此时,代码段本身是只读的,而数据段被“伪装”成只读。

  2. 触发“警报”:写入操作引发异常
    当父进程或子进程(以子进程为例)首次尝试修改某个共享的数据时,由于该内存页的权限是“只读”,CPU 会检测到这次写入操作违反了页面访问权限,从而触发一个保护故障,通常是一个缺页中断 。这个过程对进程本身是透明的。

  3. “导演”介入:操作系统处理缺页中断
    操作系统内核的缺页中断处理程序会接管控制权。它检查中断原因,发现是因为试图写入一个“只读”页面引起的。但它进一步检查发现,这个页面实际上是可写的,只是被设置了 COW 标记。

  4. 执行“拷贝”:分配新页并更新映射
    确认是 COW 场景后,操作系统会执行真正的拷贝操作 :

    • 分配新页面:为执行写入操作的进程分配一个新的物理内存页帧。
    • 复制数据:将原始共享页面中的数据复制到新分配的页面中。
    • 更新页表:修改执行写入操作的进程的页表,使其对应的虚拟地址映射到新的物理页面,并将该页面的权限设置为“可读可写”。
    • 重试指令:最后,操作系统会让引发中断的那条写入指令重新执行。此时,虚拟地址已经指向了执行写入操作的进程私有的新页面,且页面可写,写入操作便能成功完成。而原始的物理页面,因为执行写入操作的进程的页表项不再指向它,其引用计数会减 1。当没有任何进程引用原始页面时,它最终会被操作系统回收 。

因为有写时拷贝技术的存在,所以父子进程得以彻底分离,完成了进程独立性的技术保证。写时拷贝是一种延时申请技术,可以提高整体内存的使用率。

2. 进程终止

进程终止的本质时释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1 进程退出场景

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

代码运行完毕,结果正确或不正确

#include <stdio.h>    int main()    
{    printf("hello linux\n");                                                                                    return 0;                        
}    

在我们最初学编程的时候,在main函数中总要写一个return 0,但是我们却不太清楚为什么要这样写,它的作用是什么,返回的0是给谁了,可不可以不返回或返回其他的内容呢?

其实这个返回值叫做进程的退出码,用来表达该进程的运行状态,0表示运行正常结果正确,不同的返回值代表的意思也不同。

我们可以在命令行中使用echo $?来获取最近一次进程运行返回的退出码。

在这里插入图片描述

在我们学习C语言的时候可能会见过一个全局变量errno,它可以存储最近一次的错误码,这里需要注意一下,一个程序可能有多个错误码,但只能有一个退出码,当程序出现错误时,例如除零错误、空指针访问错误等,每次错误程序都会发出一个错误码,而errno可以存储最近一次的错误码,我们可以通过函数strerrno来打印一下每个错误码对应的错误信息。

#include <stdio.h>    
#include <string.h>    int main()    
{    int i = 0;    for(; i < 200; ++i)    {    printf("%d->%s\n", i, strerror(i));                                                                     }                                                                                                 
}     

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

所以我们可以通过进程的退出码来判断代码运行完毕,结果是否正确,而这个退出码其实是给父进程的,因为父进程要回收子进程的退出信息,以便用户根据退出信息做出下一步策略。

代码异常终止

代码异常终止通常是程序因外部信号或内部严重错误而被迫中止,未完成所有指令,那么此时进程的退出码便没有了意义,所以我们就不再关系进程的退出码了。

我们可以写一段无限循环代码来演示一下代码异常终止。

#include <stdio.h>    
#include <unistd.h>    int main()    
{    while(1)    {    printf("hello linux, pid: %d\n", getpid());    sleep(1);                                                                                               }                                                                                              return 0;                                                                                      
}       

我们可以通过kill 命令给该进程发送信号,使其被迫终止。

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

进程出现异常终止,本质是由于进程收到了对应的信号。

2.2 进程退出方法

正常退出

  1. 在main函数中return
  2. 在任意位置调用库函数 exit
  3. 在任意位置调用系统调用 _exit

1. 在main函数中return

在C/C++程序中,在 main函数中使用 return n;是最自然的退出方式。return的值会被作为进程的退出码(Exit Code)。惯例是返回 0表示成功,返回非零值(通常是1-255之间的整数)表示各种错误

在普通函数中return是退出该函数,只有在main函数中return才会退出进程。

2.在任意位置调用库函数 exit 和 在任意位置调用系统调用 _exit

exit 和 _exit 都会直接退出进程。它们之间的区别就是exit终止进程,会强制刷新缓冲区,但是_exit不会。

在这里插入图片描述

异常退出

ctrl + c
信号终止

3. 进程等待

3.1 进程等待的必要性

子进程退出后,若父进程未对其状态进行处理,子进程会进入僵尸状态(Z状态)。僵尸进程虽已终止,但其进程控制块(PCB)仍会保留在系统中,持续占用内存资源,进而导致内存泄漏。更关键的是,僵尸进程无法通过kill -9等常规信号清除——因为它本质上已是“死亡”进程,仅残留资源等待回收。

此外,父进程通常需要了解子进程的任务执行结果:任务是否正常完成、是否因异常(如信号中断)终止、若正常完成其返回结果是否符合预期等。这些信息的获取,也依赖于进程等待机制。

因此,进程等待的核心价值在于两方面:一是通过回收僵尸进程避免内存泄漏,二是获取子进程的退出信息以判断任务执行状态。

3.2 进程等待的方法

3.2.1 wait方法

#include<sys/types.h>
#include<sys/wait.h>pid_t wait(int*status);#返回值:
成功返回被等待进程pid,失败返回-1。#参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("I am parent process, pid: %d, ppid:%d\n", getpid(), getppid());pid_t id = fork();if(id == 0){// childint cnt = 5;while(cnt){printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);cnt--;}                                                                    exit(0);}// parentpid_t rid = wait(NULL);if(rid>0){printf("wait success pid:%d\n",rid);}return 0;
}

在这里插入图片描述

3.2.2 waitpid方法

#include<sys/types.h>
#include<sys/wait.h>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。pid = 0 ,子进程还没有退出,需要下一次再检测
pid > 0 ,  等待成功了,子进程退出,并且父进程回收成功
pid < 0 ,  等待失败

如果子进程已经退出,调⽤wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得⼦进程退出信息。
如果在任意时刻调⽤wait/waitpid,⼦进程存在且正常运⾏,则进程可能阻塞。
如果不存在该⼦进程,则⽴即出错返回。

3.3 获取子进程status

wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位):

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("I am parent process, pid: %d, ppid:%d\n", getpid(), getppid());pid_t id = fork();if(id == 0){// childint cnt = 5;while(cnt){printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);cnt--;}exit(0);}// parentint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id){int exit_code = (status >> 8) & 0xFF; // 1111 1111int exit_sig = status & 0x7F; // 0111 1111                                                                                printf("pid: %d, wait success!, status: %d, exit_code: %d, exit_sig: %d\n", getpid(), status, exit_code, exit_sig);}return 0;
}

在这里插入图片描述

3.4 阻塞等待与非阻塞等待

阻塞等待

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("我是一个父进程:pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id < 0){                                                                          perror("fork");exit(1);}else if(id == 0){// childint cnt = 5;while(cnt--){printf("I am a child process: pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);    }    exit(10);    }    else    {    int status = 0;// 父进程阻塞等待pid_t rid = waitpid(id, &status, 0);if(rid > 0){int exit_code = (status >> 8) & 0xFF;int exit_sig = status & 0x7F;printf("wait success, 退出的子进程的pid是:%d, exit_code: %d, exit_sig: %d\n", rid, exit_code, exit_sig);//if(WIFEXITED(status)) // 判断进程是否正常退出,即信号是否为0    //{    //    printf("wait success! 退出的子进程的pid是:%d, exit_code: %d\n", rid, WEXITSTATUS(status));    //}    //else    //{    //    printf("子进程是异常退出的\n");    //}    }else{printf("rid: %d\n", rid);perror("wait failed");}}
}

在这里插入图片描述

非阻塞等待

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("我是一个父进程:pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id < 0){                                                                          perror("fork");exit(1);}else if(id == 0){// childint cnt = 3;while(cnt--){printf("I am a child process: pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);    }    exit(10);    }    else    {    while(1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG); // 非阻塞检测&&回收     if(rid > 0){printf("wait success! 退出的子进程的pid是:%d, exit_code: %d\n", rid, WEXITSTATUS(status));break;}else if(rid == 0){printf("子进程正在运行,父进程还需要等待\n");usleep(200000);}else{perror("waitpid");break;}}}
}

在这里插入图片描述

4. 进程程序替换

fork()之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢?进程的程序替换来完成这个功能!

程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中!

4.1 替换原理

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

在这里插入图片描述

4.2 替换函数

#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[]);

4.2.1 函数解释

这些函数如果调⽤成功则加载新的程序从启动代码开始执⾏,不再返回。
如果调⽤出错则返回-1
所以exec函数只有出错的返回值⽽没有成功的返回值。

4.2.2 命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

l(list) : 表示参数采⽤列表
v(vector) : 参数⽤数组
p(path):有p⾃动搜索环境变量PATH
e(env):表示⾃⼰维护环境变量

#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 带 p 的,可以使⽤环境变量PATH,⽆需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要⾃⼰组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 带p的,可以使⽤环境变量PATH,⽆需写全路径execvp("ps", argv);// 带e的,需要⾃⼰组装环境变量execve("/bin/ps", argv, envp);exit(0);
}

事实上,只有execve是真正的系统调⽤,其它五个函数最终都调⽤execve,所以execve在man⼿册第2节,其它函数在man⼿册第3节。这些函数之间的关系如下图所示。
下图exec函数簇⼀个完整的例⼦:

在这里插入图片描述

5. 自主Shell命令行解释器

5.1 目标

  • 要能处理普通命令
  • 要能处理内建命令
  • 要能帮助我们理解内建命令/本地变量/环境变量这些概念
  • 要能帮助我们理解shell的些许原理

5.2 实现原理

⽤下图的时间轴来表示事件的发⽣次序。其中时间从左向右。shell由标识为bash的⽅块代表,它随着时间的流逝从左向右移动。shell从用户读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运⾏ls程序并等待那个进程结束。

在这里插入图片描述
然后shell读取新的⼀⾏输⼊,建⽴⼀个新的进程,在这个进程中运⾏程序并等待这个进程结束。
所以要写⼀个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

根据这些思路,和我们前⾯的学的技术,就可以⾃⼰来实现⼀个shell了。

5.3 源码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include <string>#define MAXSIZE 128
#define MAXARGS 32// shell自己内部维护的第一张表:命令行参数表
// 故意设计成全局的
// 命令行参数表
char* gargv[MAXARGS];
int gargc = 0;
const char* gsep = " ";// 环境变量表
char* genv[MAXARGS];
int genvc = 0;// 我们shell自己所处的工作路径
char cwd[MAXSIZE];// 最近一个命令执行完毕,退出码
int lastcode = 0;static std::string rfindDir(const std::string& p)
{if(p == "/")return p;const std::string psep = "/";auto pos = p.rfind(psep);if(pos == std::string::npos)return std::string();                                                                                                                return p.substr(pos + 1);
}void LoadEnv()
{// 正常情况,环境变量表内部是从配置文件来的// 这里我们从父进程拷贝extern char** environ;for(; environ[genvc]; ++genvc){genv[genvc] = new char[4096];                                                                                                        strcpy(genv[genvc], environ[genvc]);}genv[genvc] = NULL;printf("Load env: \n");for(int i = 0; i < genvc; ++i)printf("genv[%d]: %s\n", i, genv[i]);
}const char* GetUserName()
{char* username = getenv("USER");if(username == NULL)return "None";return username;
}const char* GetHostName()
{char* hostname = getenv("HOSTNAME");if(hostname == NULL)return "None";return hostname;}const char* GetPwd()
{char* pwd = getenv("PWD");//char* pwd = getcwd(cwd, sizeof(cwd));if(pwd == NULL)return "None";return pwd;}void PrintCommandLine()
{printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); // 用户名 @ 主机名 当前路径 fflush(stdout);
}int GetCommand(char commandline[], int size)
{if(NULL == fgets(commandline, size, stdin))return 0;// 2.1 用户输入的时候,至少会摁一下\n,abcd\ncommandline[strlen(commandline) - 1] = '\0';return strlen(commandline);
}int ParseCommand(char commandline[])
{gargc = 0;memset(gargv, 0, sizeof gargv);gargv[gargc] = strtok(commandline, gsep);while((gargv[++gargc] = strtok(NULL, gsep)));//printf("gargc: %d\n", gargc);//int i = 0;//for(; gargv[i]; ++i)//    printf("gargv[%d]: %s\n", i, gargv[i]);return gargc;
}// return val:
// 0 : 不是内建命令
// 1 :内建命令
int CheckBuiltinExecute()
{if(strcmp(gargv[0], "cd") == 0){// 内建命令                                                                                                                          if(gargc == 2){// 新的目标路径:argv[1]// 1. 更改进程内核中的路径chdir(gargv[1]);// 2. 得到更改的路径char pwd[1024];getcwd(pwd, sizeof(pwd));// 3. 更改环境变量snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);putenv(cwd);lastcode = 0;}return 1;}else if(strcmp(gargv[0], "echo") == 0){if(gargc == 2){if(gargv[1][0] == '$'){if(strcmp(gargv[1] + 1, "?") == 0){printf("%d\n", lastcode);}else if(strcmp(gargv[1] + 1, "PATH") == 0){printf("%s\n", getenv("PATH"));}}   lastcode = 0;}return 1;}return 0;
}int ExecuteCommand()
{// 不能让你的bash自己去执行命令,必须创建子进程//execvp(gargv[0], gargv);                                                                                                               pid_t id = fork();if(id < 0) return -1;else if(id == 0){// child// 让子进程去执行命令//execvp(gargv[0], gargv);execvpe(gargv[0], gargv, genv);exit(1);}else{// father// 等待子进程,回收子进程int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){//printf("wait child process success\n");lastcode = WEXITSTATUS(status);}}return 0;
}int main()
{// 0. 从配置文件中获取环境变量配置环境变量表LoadEnv();char command_line[MAXSIZE] = {0};while(1){// 1. 打印命令行字符串PrintCommandLine();// 2. 获取用户输入
if(0 == GetCommand(command_line, sizeof(command_line)))continue;//printf("%s\n", command_line);// 3. 解析字符串 -> "ls -a -l" -> "ls" "-a" "-l" // 命令行解释器,就要对用户输入的命令字符串首先进行解析!ParseCommand(command_line);// 4. 判断该命令是让父进程执行(内建命令),还是让子进程执行if(CheckBuiltinExecute())continue;// 5. 让子进程执行这个命令ExecuteCommand();}return 0;
}
http://www.dtcms.com/a/618478.html

相关文章:

  • day2江协科技-3 GPIO
  • Photoshop文字设计基础知识
  • 自己做的网站项目怎样卖微信支付 企业网站
  • Java 大视界 -- Java 大数据机器学习模型在自然语言处理中的少样本学习与迁移学习融合
  • 全椒县城乡建设局网站centos。wordpress
  • 南京华夏商务网做网站怎么样腾讯qq网页版
  • 网站怎么做白色字网站建设和执纪监督
  • Docker使用MinerU
  • 阿里巴巴国际站开店流程及费用福建网站建设
  • 云软件网站建设南昌公司网站建设公司
  • 做一个网站多久沈阳视频制作公司
  • 算法:位运算类型题目练习与总结
  • 中山网站建设中山国产服务器系统免费的有哪些
  • 【水下目标检测】Yolov8-GDFPN实现水下气泡智能识别系统
  • Python转义字符与原字符
  • 双语网站后台怎么做淄博网站快照优化公司
  • 两学一做知识竞答网站越秀营销型网站建设
  • JavaEE ——多线程的线程安全集合类
  • AI重塑语言教育的深层逻辑:从算法到应用的完整旅程
  • 递归算法精讲:从汉诺塔到反转链表
  • 详解 MySQL 自适应哈希
  • 开平市城乡建设局网站电商网站设计公司
  • 【每天一个AI小知识】:什么是支持向量机(SVM)?
  • AgentOS使用和可视化
  • 定制网站制作广州建设网站设计公司
  • 洛谷 P3935 Calculating——因数个数定理+整除分块(数论分块)
  • 哪里建设网站不会被封设计师网民
  • 人工智能综合项目开发12——模型优化与调优——损失函数
  • Linux系统安装 分区挂载时空间不足且无法回收 问题处理
  • 用curl实现Ollama API流式调用