1.进程创建补充
关于写时拷贝中的细节
1.当父进程创建子进程之时,操作系统会在页表中将父进程的数据段的读写权限改为只读权限。因此子进程创建时拷贝到的内核数据结构对应的页表中数据段也是只读权限,当父子中任意一方尝试写入数据段,即触发写入错误,但不做异常处理,而是做重新申请内存发生写时拷贝,会在物理内存中重新开辟一块空间复制对应数据段内容,再允许你进行修改,完成后返回对应物理地址改变映射关系,将父子进程页表中数据段的权限更改为可读写
2.如果对只读的代码段也进行相同操作,会被操作系统当作错误处理不发生写时拷贝
如何用代码循环的方式创建一批进程
代码解释:
1.启动程序,首先运行父进程
2.父进程进入循环,连续fork()创建 5 个子进程
3.每个子进程创建后,立即执行runChild():每 1 秒打印一次自身 PID 和父进程 PID(均为P),共打印 10 次。
4.子进程执行runChild()后立即exit(0),确保不会继续执行for循环创建新进程
5.父进程创建完所有子进程后,休眠 1000 秒(期间子进程已陆续退出),最终父进程结束。
父子进程或兄弟进程被创建进程,谁先运行不可知,完全由调度器决定
fork可能会调用失败
1.系统中有太多的进程
2.实际用户的进程数超过了限制
2.进程终止
- 进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
进程中一般父进程关心程序的运行情况,但本质是获取错误信息交由人去看,人就可以调整程序运行逻辑直到成功为止
2.1退出码
父进程(bash)获得退出码,可通过 echo ?查看,?查看,?查看,?表示命令行当中最近一个程序的退出码,退出码被保存到?当中。
为什么重复打印退出码后上一次执行的程序变成echo打印命令,成功执行打印后返回值为0
退出码的意义:
并不是所有程序都会执行打印,所以退出码是为了返回程序的运行情况,但退出码是方便计算机去查找原因,可以通过strerror来将退出码转化成对应的字符串信息方便我们去查看
该代码可以查看0到100的退出码所对应的错误信息
执行指令会变成进程去执行,也会有退出状态,也可以通过echo去查询
- errno
errno是C语言中提供的一个全局函数,用来记录程序最后一次运行错误的错误码,因为程序可能有多个报错所以errno记录最新的一次,会覆盖之前的错误码,可以根据它来查看对应的错误信息
2.2代码异常终止
代码异常终止后,进程的退出码无意义不再关心它,因为异常的本质是代码没有运行完,虽然可能返回语句执行后触发异常,但是其退出码也不再相信其准确性,更应该关心的是为什么触发异常并且发生了什么异常
进程出现异常,本质是进程收到了对应的信号,可以通过kill -l指令去查看这些信号
可以通过kill指令发送信号来模拟进程异常退出
2.3exit与_exit
exit()中数字即为程序的退出码,在main函数中与return等价
exit与return有什么区别?1.return在main函数中表示进程结束,其他函数中表示函数结束并返回,进程继续运行
2.exit在任意位置被调用都表示进程直接退出
_exit与exit使用方法一样,都在被调用时终止进程,区别在于后者会刷新缓冲区中内容但前者不会
1.缓冲区可以明确知道它一定不在内核中,如果在的话调用_exit时一定也会刷新缓冲区,操作系统不做浪费空间和时间的事情,既然内核调用_exit时不刷新缓冲区你,那么就没必要再内核中维护它
2.缓冲区在用户空间内,后续学习基础IO时会再详解
_exit()是系统调用函数,由内核直接调用不会刷新缓冲区,exit()是C语言提供的接口,刷新完缓冲区中数据后会调用_exit()来终止进程。
3.进程等待
1.是什么
通过系统调用wait/waitpid,来进行对子进程状态检测与回收的功能!
2.为什么
1.僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题
2.需要通过进程等待来获取子进程的退出情况,需知道我给子进程布置的任务它完成的怎么样了,是否进行等待是可选的
3.1wait方法
wait等待一个子进程退出场景
头文件:1.#include<sys/types.h> 2. #include<sys/wait.h>
形式:pid_t wait(int*status);
返回值: 成功返回被等待进程pid,失败返回-1。
参数:输出型参数(把函数内部值带出来的参数叫做输出型参数),获取子进程退出状态,不关心则可以设置成为NULL
#include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
pid_t id=fork();
10 if(id<0){
11 perror("fork");
12 return 1;
13 }
14 else if(id==0)
15 {//子进程
16 int cnt=5;
17 while(cnt)
18 {
19
20 printf("I am child, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
21 cnt--;
22 sleep(1);
23 }
24 exit(0);
25 }
26 else{//父进程
27 int cnt=10;
28 while(cnt)
29 {
30 printf("I am father, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
31 cnt--;
32 sleep(1);
33 }
34
35 pid_t ret=wait(NULL);
36 if(ret==id)
37 {
38 printf("wait success, ret: %d\n",ret);
39 }
40 sleep(5);
41 }
42 return 0;
43 }
代码解释:
1.perror 会将上一个系统调用的错误信息(存储在全局变量 errno 中)转换为对应的错误描述字符串,并输出到标准错误流(通常是终端),
2.fork创建子进程后,子进程先执行5s然后退出,父进程再执行10s然后利用wait等待回收子进程退出信息,父进程再等待5s后退出
多进程采取循环wait的方法回收子进程退出信息,结合进程地址空间文章当中创建多i进程的方式
3.2获取子进程status
1.wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
2.如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
1.如何知道子进程代码是否异常?
代码异常终止的本质是收到信号,所以用低7位看是否为0来判断是否收到信号,若全为0则证明没有异常(因为没有0号信号,全为0证明没有收到信号),反之。第8位标志后续会学习
2.子进程没有异常,如何知道结果正确与否?
通过次低8位来判断对应的退出码,查看退出信息可通过位运算的方式来获取上述两种结果
1.前缀0x表示16进制数,与status采取低16位运算相符
2.0x7F前8位为0,后八位为0111 1111刚好保留低八位来计算结果,其余位通过&运算都为0
3.0xFF前8位都为0,后八位为全1,通过&运算来获取退出码的信息
为了方便,设计者设定宏来表示status两个不同的位状态
1.WIFEXITED(status):表示低八位,查看是否出现异常退出的情况(返回值非0为真,0为假)
2.WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码并返回。
父进程要拿子进程的状态数据和其他数据,为什么必须要用wait等系统调用呢?
因为进程具有独立性,即使你通过更改全局变量获取子进程退出码,父进程无法收到,只能通过系统帮忙获取对应pid子进程的退出信息
必须通过管理者拿到被管理者的数据,不能通过用户直接在操作系统拿到他人数据,因为操作系统不相信任何人
注意:
1.当等待的不是自己的子进程时wait和waitpid方法就会调用失败
2.wait和waitpid是系统调用函数,调用时系统会自动找到其对应的子进程,一旦子进程退出,就会把其退出信息拷贝到status所对应的指针当中
3.3waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
id_t 是在 C/C++ 语言中用于表示进程 ID的数据类型
返回值:
大于0:成功收集到子进程的id
等于0:还在等待,条件还未就绪
小于0:等待失败
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 查看进程是否是正常退出
WEXITSTATUS(status): 查看进程的退出码
options:
0代表父进程处于阻塞状态,一直等待子进程退出信息
WNOHANG:非阻塞轮询,当子进程未结束时,父进程在等待的同时可以做自己的事情然后循环查询子进程是否退出,等待其退出信息直到获取为止
使用方法与wait相同,只需注意参数和返回值
关于options:
- 非阻塞退出方式
#include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 //非阻塞轮询
8 int main()
9 {
10 pid_t id=fork();
11 if(id<0){
12 perror("fork");
13 return 1;
14 }
15 else if(id==0)
16 {//子进程
17 int cnt=30;
18 while(cnt)
19 {
20
21 printf("I am child, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
22 cnt--;
23 sleep(1);
24 }
25 exit(0);
26 }
27 else{//父进程
28 int cnt=5;
29 while(cnt)
30 {
31 printf("I am father, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
32 cnt--;
33 sleep(1);
34 }
35
36 int status=0;
37 while(1){
38 pid_t ret=waitpid(id,&status,WNOHANG);
39 if(ret>0)
40 {
41 printf("wait success, ret: %d, exit sig:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
42 if(WIFEXITED(status))
43 {
44 printf("进程是正常跑完的,退出码为:%d\n",WEXITSTATUS(status));
45 }
46 else{
printf("进程出异常了\n");
48 }
49 break;
50 }
51 else if(ret<0)
52 {
53 printf("wait failed!\n");
54 break;
55 }
56 else{//ret==0
57 printf("子进程还没退出,我在等等吧...\n");
58 sleep(1);
59 }
60 }
61 }
62 return 0;
63 }
- 非阻塞轮询demo演示
#define TASK_NUM 10
typedef void(*task_t)();
task_t tasks[TASK_NUM];
void task1()
{
printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
}
void task2()
{
printf("这是一个执行检测网络健康状态的一个任务, pid: %d\n", getpid());
}
void task3()
{
printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
}
int AddTask(task_t t);
// 任务的管理代码
void InitTask()
{
for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
AddTask(task1);
AddTask(task2);
AddTask(task3);
}
int AddTask(task_t t)
{
int pos = 0;
for(; pos < TASK_NUM; pos++) {
if(!tasks[pos]) break;
}
if(pos == TASK_NUM) return -1;
tasks[pos] = t;
return 0;
}
void DelTask()
{}
void CheckTask()
{}
void UpdateTask()
{}
void ExecuteTask()
{
for(int i = 0; i < TASK_NUM; i++)
{
if(!tasks[i]) continue;
tasks[i]();
}
}
1.基本测试代码与非阻塞状态相同,添加一些上述代码来模拟父进程等待子进程退出时所做的工作,可以在进入非阻塞轮询状态前完成一些工作的初始化,在调用waitpid的返回值为0的情况下来执行一些父进程本身的轻量化工作
2.多进程情况下将waitpid的参数pid改为-1,即回收任意子进程,同时在非阻塞轮询循环中等待方法调用成功或失败的条件语句中,break应该替换为子进程数–,可用计数器来维护
总结:
1.当父进程调用等待方法时,核心任务是回收子进程退出信息,所以要求非阻塞轮询时,在等待期间做自己任务时任务量不能太重,执行时间不能太长,以免本末倒置,当然回收子进程不是一定要一退出立马回收,可以适当延迟一会
2.父进程最先开始运行,也要最后一个退出,这样才能回收所有子进程的退出信息,而正确调用等待方法可以保证父进程最后一个退出
4.进程程序替换
linux中所有进程一定是某个进程的子进程,命令行中的进程一定都是bash的子进程,程序启动时都先调用exec系列函数
4.1原理
进程被创建时会创建自己的内核数据结构,通过页表映射虚拟地址到物理内存找到相应的数据和代码来运行,进程替换就是将物理内存中该进程的代码和数据替换成目标代码和数据,空间不够再开辟,多了就释放,再从新代码的函数入口处重新执行一遍。
注意:
1.进程替换时代码也可以发生写时拷贝,因为调用的是系统接口,操作系统会在物理内存中重新开辟一块空间允许你修改代码,我们无法做到在物理内存中对只读的代码进行写入更改,但操作系统可以
2.程序替换不创建新进程,只进行进程的程序代码和数据的替换工作
补充:
1.现象:
程序替换成功后,exec后续的代码不会执行(已经被替换掉);替换失败才可能执行后续代码,exec函数只有失败返回值,没有成功返回值。
2.cpu如何得到程序的入口地址?
Linux中形成的可执行程序是有格式的,ELF,可执行程序的表头和入口地址就在表中
4.2各程序替换接口
exec系列总共有7个函数,用于进程替换,其中execve是唯一的系统调用函数,其他6个函数均通过它实现
1.主要区别在于参数格式和是否搜索环境变量
2.函数名中字母含义:
l(list):参数用可变参数列表传递;
v(vector):参数用数组传递;
p(path):自动从 PATH 搜索程序路径;
e(environment):允许自定义环境变量。
3.注意事项:
返回值:
成功时不会返回(因为进程已被替换);失败时返回 -1,并设置 errno。
进程属性:
替换后,进程 ID(PID)、父进程 ID(PPID)、信号掩码等内核属性保持不变,但代码、数据、堆、栈等用户空间内容被完全替换。
使用场景:
通常与 fork() 配合,父进程创建子进程后,子进程通过 exec 系列函数加载新程序(如 shell 执行命令的原理)。
4.参数解析:
path:是新程序的完整路径
const char *arg, … :是可变参数列表,必须以NULL结尾
file 是新程序的文件名(如 ls),会自动从环境变量 PATH 中搜索路径
envp 是自定义环境变量数组(替代进程原有的环境变量)
argv 是命令行参数数组(以 NULL 结尾),替代可变参数列表。
写法分析:
看到ls出现了两次,为什么?
1.因为执行一个程序时第一件事是先找到这个程序,所以第一个参数可通过具体路径或文件名直接在环境变量中寻找
2.找到可执行程序后,还要知道如何执行它,执行它的哪些功能,就需要涵盖一些选项。标准写法就是在命令行中怎么写,就怎么当参数传上去,区别只是将空格替换为逗号,然后每一部分字符用双引号括起来。
靠可变参数列表或替代它的命令行参数数组来实现
如图中红框所示,ls指令执行时也会变成进程,也有命令行参数,而myargv这个自定义命令行参数数组会把参数传给ls,让它知道执行哪一部分功能,体现了exec系列函数作为加载器的效果
其它函数接口不过多演示,用法都一样,注意参数的区别即可
4.2.1执行创建的命令demo演示
- exec系列函数可以执行系统命令,也可以执行自己的命令
otherexe.cpp文件:
1 #include<iostream>
2 using namespace std;
W> 4 int main(int argc,char*argv[],char*env[])
5 {
6 cout<<argv[0]<<"begin running"<<endl;
7 cout<<"这是命令参数:\n";
8 for(int i=0;argv[i];i++)
9 {
10 cout<<i<<":"<<argv[i]<<endl;
11 }
12 cout<<"这是环境变量信息:\n";
13 for(int i=0;env[i];i++)
14 {
15 cout<<i<<":"<<env[i]<<endl;
16 }
17 cout<<argv[0]<<"stop running"<<endl;
18 return 0;
19 }
mycmd.c文件:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9 extern char **environ;
10 pid_t id=fork();
11 if(id==0){//子进程成功创建
12
19 char*const myargv[]={
W> 20 "otherexe",
W> 21 "-a",
W> 22 "-b",
W> 23 "-c",
24 NULL
25 };
26 printf("before: I am process,pid:%d,ppid:%d\n",getpid(),getppid());
29 execve("./otherexe",myargv,environ);
30 printf("after: I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
31 exit(1);//用来标识是否替换成功
32 }
33 pid_t ret=waitpid(id,NULL,0);
34 if(ret>0) printf("wait success,father pid:%d,ppid:%d\n",getpid(),getppid());
35 sleep(3);
36 return 0;
37 }
这是上述两个文件经过编译后形成可执行文件的运行结果
自己所创建的命令一样可以执行!
mycmd.c文件中environ 是一个指向字符指针数组的外部变量,用于访问进程的环境变量列表
当要生成两个可执行文件时,可以在同一个自动化构建工具中使用伪目标;也可以用两个不同名的自动化构建工具通过make -f 文件名来区分使用。
4.2.2脚本替换demo演示
1.#!/bin/bash 是 Shell 脚本文件开头的特殊标记,称为Shebang(也叫 Hashbang),它的作用是指定该脚本文件应该使用哪个解释器来执行
2.#!:这是 Shebang 的固定起始符号,用于告知系统接下来的内容是用于指定脚本解释器的路径,当用户尝试执行一个脚本文件时,系统会先检查文件开头是否存在 Shebang。如果存在,就会按照其指定的路径去寻找对应的解释器程序来执行脚本内容。
3./bin/bash:这是一个文件路径,表示用于执行脚本的 Shell 解释器的位置
mycmd.c文件与执行自己文件的代码演示除下图外都相同
脚本语言不是脚本在跑,而是由脚本解释器解释执行的。由我们的命令行解释器bash从对应脚本文件中一行一行的拿出来来执行
运行结果如下
结论:
无论是自己的可执行程序,还是脚本或跨语言调用(所有语言与运行起来本质都是进程)都可以通过进程替换运行起来
4.2.3给子进程传递环境变量
- 1.新增环境变量
1.已知环境变量具有全局属性,能被子进程所继承,除了直接在bash命令行上创建
2.bash不直接新增环境变量而是由其子进程来新增,还可以在父进程的地址空间上直接putenv创建环境变量,添加到调用它的进程当中
1.putenv是一个用于修改或添加环境变量的函数,常用于在程序运行时设置或更新环境变量,动态性
2.参数:
string 是一个格式为 “变量名=值” 的字符串
3.返回值:
成功返回 0;失败返回非 0,并设置 errno 标识错误原因。
4.工作原理:
进程的环境变量列表由全局变量 environ 维护,若 string 中的 “变量名” 已存在于环境变量列表中,则替换其值为新值;若 “变量名” 不存在,则添加该字符串到环境变量列表末尾。
注意:
putenv 不会复制 string 指向的字符串,而是直接将该指针存入 environ 数组。因此,string 必须是全局变量或动态分配的内存(不能是栈上的局部变量,否则函数返回后内存释放会导致环境变量失效)
测试代码与4.2.1和4.2.2中一样,多出putenv语句,
结果是子进程中打印出来了新创建的环境变量信息,同时bash命令行中查看不到
- 2.彻底替换
基础测试代码与前文不变
可以发现对比之前环境变量值被自己定义的值所覆盖
5.自定义shell
自定义shell命令行解释器需要以下过程:
- 获取命令行
- 解析命令行
计算机只能识别结构化的参数格式,而用户输入的是自由文本。解析步骤将文本转换为程序可识别的格式,为后续的命令执行(尤其是 execvp 函数)提供正确的输入。
- 建立一个子进程(fork)
通过 fork() 创建一个与父进程(Shell 本身)几乎完全相同的子进程。如果直接在 Shell 进程中执行命令(不创建子进程),当执行 execvp 时,Shell 进程的代码和数据会被目标命令替换,导致 Shell 自身退出(用户无法再输入新命令)。
而通过 fork 创建子进程后,子进程负责执行命令,父进程(Shell)可以继续等待下一条命令,确保 Shell 始终处于 “就绪状态”。
- 替换子进程(execvp)
fork 创建的子进程只是 “复制了 Shell 的副本”,本身并没有执行目标命令。execvp 的作用是 “替换子进程的内容”,使其成为真正的目标命令进程。
- 父进程等待子进程退出(wait)
5.1全局变量和宏的定义
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表
定义全局变量是为了在不同函数间共享状态和数据,确保命令执行的连续性和环境的一致性
1.int lastcode = 0;
作用:存储上一条命令的退出状态码
当执行 echo $? 时,需要访问上一条命令的结果,而 lastcode 会被 wait 函数(在父进程等待子进程时)更新,同时被 echo 命令的处理函数读取。全局变量确保了这种 “写入 - 读取” 的跨函数操作。
2.int quit = 0;
作用:控制 Shell 主循环的退出标志(如用户输入 exit 时,设置 quit = 1 终止循环)。
3.extern char **environ;
作用:声明系统环境变量表(全局变量),用于访问和修改进程的环境变量(如 getenv、putenv 等函数依赖它)。
环境变量是进程级别的全局状态,需要在 Shell 启动到退出的整个生命周期中被所有命令(如 export、echo $PATH 等)共享和修改,因此必须以全局形式访问。
4.char commandline[LINE_SIZE];
作用:存储用户输入的原始命令行字符串
通常由读取命令的函数(如 fgets)写入,再由解析函数(如拆分 argv)读取。全局变量避免了在函数间传递大型字符数组的麻烦。
5.char *argv[ARGC_SIZE];
作用:存储解析后的命令参数列表
6.char pwd[LINE_SIZE];
作用:存储当前工作目录的路径(类似环境变量 PWD 的本地缓存)。
当执行 cd 命令时,chdir 函数修改进程工作目录后,需要更新 pwd 变量,并可能同步到环境变量 PWD。全局变量确保 pwd 能被 cd 处理函数修改,并被其他需要路径的函数(如提示符显示)读取。
7.char myenv[LINE_SIZE]; 和 自定义本地变量表
作用:存储用户通过 export 命令设置的环境变量字符串,或其他本地变量
putenv 函数要求环境变量字符串的内存长期有效(不能是栈变量),全局数组 myenv 提供了稳定的内存空间。本地变量表也需要在多次命令执行中保持,因此需要全局存储。
5.2shell的交互界面与命令解析
const char *getusername()
{
return getenv("USER");
}
const char *gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd, sizeof(pwd));
}
1.通过getenv环境变量 USER 获取当前登录用户的用户名
2.通过getenv环境变量 HOSTNAME获取当前主机名
注意:标准库中已有 gethostname 系统调用,这里是自定义函数,通过环境变量实现更简单的主机名获取。
3.调用 getcwd 函数获取当前工作目录的绝对路径,并存储到全局变量 pwd 中。第一个参数是存储路径的缓冲区,第二个参数是缓冲区大小。成功返回缓冲区地址,失败返回 NULL
void interact(char *cline, int size)
{
getpwd();
// 打印提示符(用户名@主机名 当前目录 $)
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
// 读取用户输入的命令行
char *s = fgets(cline, size, stdin);
assert(s);// 确保输入成功
(void)s;// 消除未使用变量警告
// "abcd\n\0"
cline[strlen(cline)-1] = '\0';
}
关于宏的打印:
1.若宏本身是字符串常量(定义时带引号),printf 中直接使用宏名,不加双引号
2.若宏是非字符串,printf 中需要用双引号包含格式控制符,宏作为参数传入
3.特殊需求(打印宏名)需使用字符串化操作符 #
cline[strlen(cline)-1] = ‘\0’;作用:
去除字符串 cline 末尾的换行符 \n,当使用 fgets 从标准输入(键盘)读取用户输入时,函数会将用户输入的内容(包括按下回车键产生的换行符 \n)一起存入字符数组。例如:用户输入 hello 并按回车,数组中实际存储的是 hello\n\0,strlen(cline) 计算字符串 cline 的长度(不包含终止符 \0)
int splitstring(char cline[], char *_argv[])
{
int i = 0;
// 第一次调用strtok:以DELIM为分隔符拆分cline,获取第一个参数(命令名)
argv[i++] = strtok(cline, DELIM);
// 循环调用strtok:继续拆分剩余部分,直到NULL(无更多参数)
while(_argv[i++] = strtok(NULL, DELIM));
return i - 1; // 返回参数个数(含命令名)
}
strtok函数:char *strtok(char *str, const char *delim);
str:首次调用时传入要分割的字符串;后续调用传入 NULL,表示继续分割上一次的字符串
delim:分隔符集合(字符串),包含所有可能作为分隔符的字符
特点:
strtok 是状态化函数,内部会保存上次分割的位置
注意:
1.strtok 会直接修改原字符串,将分隔符替换为 \0,因此不能传入字符串常量
2.多个连续的分隔符会被视为一个分隔符
5.3执行普通命令
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0){
perror("fork");
return;
}
else if(id == 0){
//让子进程执行命令
//execvpe(_argv[0], _argv, environ);
execvp(_argv[0], _argv);
exit(EXIT_CODE);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
通过fork创建子进程,子进程任务进行程序替换执行新程序,父进程任务阻塞等待子进程退出, _argv[0] 是要执行的程序路径或名称
5.4内健命令处理
int buildCommand(char *_argv[], int _argc)
{
if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
chdir(argv[1]);// 切换到指定目录(argv[1] 是目标路径)
getpwd(); // 获取当前工作目录(自定义函数,推测会将结果存入全局变量 pwd)
sprintf(getenv("PWD"), "%s", pwd);// 更新环境变量 PWD 为当前目录
return 1;// 表示已处理,无需外部执行
}
}
else if(_argc == 2 && strcmp(_argv[0], "export") == 0){
strcpy(myenv, _argv[1]);// 将参数(如 "KEY=VALUE")复制到全局变量 myenv
putenv(myenv);// 将 myenv 添加到环境变量中
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
if(strcmp(_argv[1], "$?") == 0)
{ // 输出上一条命令的退出码(lastcode),然后重置为 0
printf("%d\n", lastcode);
lastcode=0;
}
else if(*_argv[1] == '$'){// 输出环境变量的值
char *val = getenv(_argv[1]+1); // _argv[1]+1 跳过 '$' 符号
if(val) printf("%s\n", val);
}
else{// 直接输出普通字符串
printf("%s\n", _argv[1]);
}
return 1;
}
// 特殊处理一下ls
if(strcmp(_argv[0], "ls") == 0)
{
_argv[_argc++] = "--color";// 追加 "--color" 参数(让 ls 输出带颜色)// 保持参数数组以 NULL 结尾(符合 exec 函数要求)
_argv[_argc] = NULL;
}
return 0;
}
1.cd 必须由 Shell 进程自身执行,因为子进程无法改变父进程(Shell)的工作目录,切换目录后同步更新 PWD 环境变量,符合 Shell 行为习惯
2.处理echo的条件判断语句中,当用户在 Shell 中输入类似 echo PATH的命令时:命令参数数组下划线argv的实际内容是["echo","PATH 的命令时:命令参数数组 下划线argv 的实际内容是 ["echo", "PATH的命令时:命令参数数组下划线argv的实际内容是["echo","PATH", NULL],为什么用 _argv[1]+1 而不是 _argv[2]?整个环境变量引用(如 $PATH)是作为一个参数传给 echo 的,存储在 _argv[1] 中,_argv[1]+1 表示指针向后移动一个字符,指向 “PATH”(去掉了 $)
5.5main函数
int main()
{
while(!quit){
// 1. 交互问题,获取命令行
interact(commandline, sizeof(commandline));
// commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
// 2. 子串分割的问题,解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
// 3. 指令的判断
// debug
//for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
//4.处理内键命令,本质就是一个shell内部的一个函数
int n = buildCommand(argv, argc);
// ls -a -l | wc -l,预留管道命令处理逻辑
// 4.0 分析输入的命令行字符串,获取有多少个|, 命令打散多个子命令字符串
// 4.1 malloc申请空间,pipe先申请多个管道
// 4.2 循环创建多个子进程,每一个子进程的重定向情况。最开始. 输出重定向, 1->指定的一个管道的写端
// 中间:输入输出重定向, 0标准输入重定向到上一个管道的读端 1标准输出重定向到下一个管道的写端
// 最后一个:输入重定向,将标准输入重定向到最后一个管道的读端
// 4.3 分别让不同的子进程执行不同的命令--- exec* --- exec*不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向
// 5. 普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
命令行本质就是字符串,打印出来的和等待你输入的都是字符串,shell本质是死循环,不断等待新指令的输入。while(!quit){ … },这是一个无限循环,直到 quit 被设为 true(如用户输入 exit 命令)才会退出,模拟了 Shell 程序 “持续等待并处理命令” 的特性。