先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
在服务器资源即将被吃完时,卡住了,我们一般称服务器hang
住了,进而导致宕机
。所以 W 表示等待,NO 表示不要,HANG 表示卡了,所以这个宏的意思是等待时不要卡住。
如何理解父进程等子进程中的 “ 等 ” ❓
所谓的等并不是把父进程放在 CPU 上,让父进程在 CPU 上边跑边等。本来父子进程都在运行队列中等待 CPU 运行,当子进程开始被 CPU 运行后,就把父进程由 R 状态更改为 !R 状态,并放入等待队列中,此时父进程就不运行了,它就在等待队列中等待。当子进程运行结束后,操作系统就会把父进程放入运行队列,并将状态更改为 R 状态,让 CPU 运行,这个过程叫做唤醒等待
的过程。
操作系统是怎么知道子进程退出时就应该唤醒对应的父进程呢 ❓
wait 和 waitpid 是系统函数,是由操作系统提供的,你是因为调用了操作系统的代码导致你被等待了,操作系统当然知道子进程退出时该唤醒谁。
这里,我们只要能理解等待就是将当前进程放入等待队列中,将状态设置为 !R 状态。所以一般我们在平时使用计算机时,肉眼所发现的一些现象,如某些软件卡住了,根本原因是要么进程太多了,导致进程没有被 CPU 调度;要么就是进程被放到了等待队列中,长时间不会被 CPU 调度。我们曾经在写 VS 下写过一些错误代码,一旦运行,就会导致 VS 一段时间没有反应。所谓的没有反应就是因为程序导致系统出现问题,操作系统在处理问题区间,把 VS 进程设置成 !R 状态,操作系统处理完,再把 VS 唤醒。
验证子进程僵尸后,退出结果会保存在 PCB 中 ❓
可以看到在 Linux 2.6.32 源码中,task_struct 里包含了退出码和退出信息。
✔ 测试用例一:
同 wait 测试用例二。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(0);
}
//father
sleep(8);
pid_t ret = waitpid(id, NULL, 0);//只有一个进程,同waitpid(-1, NULL, 0)
printf("father wait done, ret: %d\n", ret);
sleep(3);
return 0;
}
💨运行结果:
✔ 测试用例二:
父进程 fork 派生一个子进程干活,父进程通过 status 拿到子进程的退出码,可以知道子进程把活做的怎么样。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
//exit(0);//做好了
exit(123);//没做好
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
printf("%d\n", status);
printf("father wait done, ret: %d, exit code: %d\n", ret, code);
if(code == 0)
{
printf("做好了\n");
}
else
{
printf("没做好\n");
}
return 0;
}
💨运行结果:
(31488)10 = (0111 1011 0000 0000)2 ;
0111 1011 0000 0000 >> 8 = 0111 1011;
(0111 1011)2 = (123)10 ;
子进程已经退出了,子进程的退出码放在哪 ❓
换句话说,父进程通过 waitpid 要拿子进程的退出码应该从哪里去取呢,明明子进程已经退出了。子进程是结束了,但是子进程的状态是僵尸,也就是说子进程的相关数据结构并没有被完全释放。当子进程退出时,进程的 task_struct 里会被填入当前子进程退出时的退出码,所以 waitpid 拿到的 status 值是通过 task_struct 拿到的。
✔ 测试用例三:
针对测试用例二,父进程无非就是想知道子进程的工作完成的结果,那全局变量是否可以作为子进程退出码的设置,以此告知父进程子进程的退出码。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int code = 0;
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
code = 123;
exit(0);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
printf("father wait done, ret: %d, exit code: %d\n", ret, code);
if(code == 0)
{
printf("做好了\n");
}
else
{
printf("没做好\n");
}
return 0;
}
💨运行结果:
很显然,不可以。这里对于全局变量,发生了写时拷贝,在进程地址空间里我们说过父子是具有独立性的,虽然变量是同一个,但实际上子进程或父进程所写的数据,它们都是无法看到彼此的,所以不可能让父进程拿到子进程的退出结果。
✔ 测试用例四:
模拟异常终止 —— 野指针。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
//err
int\* p = 0x12345;
\*p = 100;
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
int sig = status & 0x7F;//0111 1111
printf("father wait done, ret: %d, exit code: %d, sig: %d\n", ret, code, sig);
return 0;
}
💨运行结果:
子进程崩溃后,立马退出,变成僵尸,并不会影响父进程,这叫做父子具有独立性,父进程等待成功 (不管你是正常还是非正常退出),随后进行回收。此时子进程的退出码是无意义的,子进程的异常终止导致父进程获得了子进程退出时的退出信号,我们发现它的信号是第 11 号信号(SIGSEGV),它一般都是段错误。
✔ 测试用例五:
模拟异常终止 —— 使用kill -9
信号亲手杀死子进程。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 50;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
int sig = status & 0x7F;//0111 1111
printf("father wait done, ret: %d, exit code: %d, sig: %d\n", ret, code, sig);
return 0;
}
💨运行结果:
当我们把正在运行的子进程亲手杀掉后,父进程立马做回收工作,此时退出码是什么已经不重要了,父进程拿到的信号是第 9 号信号(SIGKILL),此时我们就知道子进程连代码都没跑完,是被别人杀掉才退出的。
✔ 测试用例六:
父进程完整的等待子进程的全过程。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
int\* p = 0x12345;
\*p = 100;
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("wait success!\n");
if((status & 0x7F) == 0)//正常退出
{
printf("process quit normal!\n");
printf("exit code: %d\n", (status >> 8) & 0xFF);
}
else//信号退出
{
printf("process quit error!\n");
printf("sig: %d\n", status & 0x7F);
}
}
return 0;
}
💨运行结果:
正常,
异常,
✔ 测试用例七:
可以看到需要对数据进行加工才可以获取退出码和退出信号,比较麻烦,我们一般也不会自己加工。其实系统有提供一些宏(函数),可以直接使用,我们主要学习 3 个 —— WIFEXITED(status)、WEXITSTATUS(status)、WTERMSIG(status),其相关介绍可在 waitpid 手册里查找。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
//err
//int\* p = 0x12345;
//\*p = 100;
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("wait success!\n");
if(WIFEXITED(status))//查看进程是否正常退出,是则真,不是则假。同(status & 0x7F) == 0
{
printf("normal quit!\n");
printf("quit code: %d\n", WEXITSTATUS(status));//查看进程退出码。同(status >> 8) & 0xFF
}
else
{
printf("process quit error!\n");
printf("sig: %d\n", WTERMSIG(status));//查看导致子进程终止的信号的编号。同status & 0x7F
}
}
return 0;
}
💨运行结果:
正常,
异常,
✔ 测试用例八:
非阻塞等待。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 3;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
//err
//int\* p = 0x12345;
//\*p = 100;
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father 非阻塞等待
int status = 0;
while(1)
{
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret == 0)//等待失败,继续等待
{
printf("wait next!\n");
printf("father do other thing!\n");
}
else if(ret > 0)//等待成功
{
printf("wait success, ret: %d, code: %d\n", ret, WEXITSTATUS(status));
break;
}
else//等待失败,真的失败
{
printf("wait failed!\n");
break;
}
}
//father 阻塞等待
//int status = 0;
//pid\_t ret = waitpid(id, &status, 0);
//if(ret > 0)
//{
// printf("wait success!\n");
// if(WIFEXITED(status))
// {
// printf("normal quit!\n");
// printf("quit code: %d\n", WEXITSTATUS(status));
// }
// else
// {
// printf("process quit error!\n");
// printf("sig: %d\n", WTERMSIG(status));
// }
//}
return 0;
}
💨运行结果:
三、进程替换
💦 为什么要进程替换 && 什么是进程替换
创建子进程的目的:
- 执行父进程的部分代码
我们之前所写的代码都属于这种情况。
- 执行其它程序的代码
不要父进程的代码和数据。所以我们要学习进程替换。
所以进程替换是为了子进程能够执行其它程序的代码;进程替换就是以写时拷贝的策略,让第三方进程的代码和数据替换到父进程的代码和数据,给子进程用,因为进程间具有独立性,所以不会影响父进程。以前我们说数据是可写的,代码是不可写的,现在看来代码确实是不可写的,但是接下来要把其它程序的代码放在内存里让子进程看到,以前子进程看到的是父进程的代码,现在有新的代码来了,此时也要对代码进行写时拷贝。99% 的情况是对数据进行写时拷贝,代码只读。1% 的情况代码依旧是只读,本质就是对父进程不可写,而子进程后续调用某些系统调用,实际是给子进程重新开辟空间,把新进程的代码加载,此时就不让子进程执行父进程的代码,这个过程叫做程序替换。
💦 替换原理
我们想让子进程里执行新的程序,可以一步到位在内存里重新开辟两块空间以加载新程序的代码和数据,再修改子进程页表的映射关系,之后父子就彻底脱离了,这个过程就是进程替换。
系统是如何做到重新建立映射关系的呢 ❓
当子进程里要加载新进程时,操作系统可以设置一些特殊信号让该进程对全部代码和数据的写入,子进程会自动触发写时拷贝,重新开辟空间,再重新把代码和数据加载。
在进行进程替换时,有没有创建新的进程 ❓
我们并不需要重新开辟新的 PCB、地址空间、页表,没有创建新进程的最有力证据是 pid 没变。所以我们曾经说过,程序要运行起来,必须先加载到内存,这句话当然没问题。但是反过来,程序只要加载到内存了,一定是变成一个进程,这句话有纰漏,因为进程是否是新创建是不一定的。不过大部分情况下是创建新进程的,进程替换是属于少数。
所以进程替换不会改变进程内核的数据结构,只会修改部分页表数据,然后把新进程的代码和数据加载至内存,重新构建页表映射关系,和父进程彻底脱离。
💦 替换函数
其实严格来说有7种以exec开头的系列函数,统称exec函数:
#include<unistd.h>
int execl(const chaar* path, const char* arg, ...);
int execlp(const char* file, const chr* arg, ...);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execle(const chra* path, const char* arg, ..., char* const envp[]);
int execve(const char* path, char* const argv[], char* const envp[]);
int execvpe(const char* file, char* const argv[], char* const envp[]);
这些函数的功能都是一样的,如果用 C++ 去设计这样的接口,一定是重载。这里是使用 C 去设计的,函数名的命名也有区分。下面我们会对这些接口进行演示,但实际在后面常用的也只是部分而已。
为什么 execve 要单独拎出来 ❓
虽然头文件都是 <unistd.h>,但实际上真正是系统提供函数只有 execve,其余的 6 个都是封装的,最后底层调用的依旧是 execve,这样做的原因是需要根据不同的用户来定制不同的使用场景。好比,大家最后吃的米饭都会转换成能量,但是有的人喜欢蛋炒饭、有的人喜欢肉丝炒饭。
💦 函数解释及使用
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- 如果调用出错则返回 -1
- 所以 exec 函数只有出错的返回值而没有成功的返回值
✔ 测试用例一:
单进程,父进程亲自干活:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("my process begin!\n");
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
printf("my process end!\n");
return 0;
}
多进程,父进程创建子进程干活:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
exit(1);//这里一定是替换失败
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
}
else
{
printf("wait error!\n");
}
return 0;
}
💨运行结果:
为什么图一没有输出 my process end ! && 图二的退出码是 0 ❓
因为在这之前 execl 已经程序替换了,所以 execl 后面的代码已经不是当前进程的代码了,所以图二获取到的退出码 0 是 ls 的退出码。换言之,一旦程序替换,你到底执行正确与否是取决于 ls 程序。
所以 exec 系列的函数不用考虑返回值,只要返回了,一定是这个函数调用或程序替换失败了。注意编程规范是父进程创建子进程干活。
加载器 ❓
一个完整的集成开发环境的组件肯定包括编辑器、编译器、调试器、加载器等。一个软件被加载到内存里,肯定是运行起来,形成进程,进程再调用 exec 系列的函数就可以完成加载的过程。所以 exec 可以理解成一种特殊的加载器。
✔ 测试用例二:
execv 与 execl 较为类似,它们的唯一差别是,如果需要传多个参数,那么:execl 是以可变参数的形式进行列表传参;execv 是以指针数组的形式传参。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//const char\* const my\_argv[] = {"ls", "-l", "-a", "-i", NULL};//err,注意要与函数原型的参数类型char\* const argv[]匹配
char\* const my_argv[] = {"ls", "-l", "-a", "-i", NULL};
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
execv("/usr/bin/ls", my_argv);
exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);//同WTERMSIG(status), WEXITSTATUS(status)
}
else
{
printf("wait error!\n");
}
return 0;
}
💨运行结果:
✔ 测试用例三:
execlp 相比 execl 在命名上多了 1 个 p,且参数只有第 1 个不同:不同点在于 execlp 不需要带路径,execlp 在执行时,它会拿着你要执行的程序自动的在系统 PATH 环境变量中查找你要执行的目标程序。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
execlp("ls", "ls", "-a", "-i", "-l", NULL);
exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("child status -> sig: %d, code: %d\n", WTERMSIG(status), WEXITSTATUS(status));
}
else
{
printf("wait error!\n");
}
return 0;
}
💨运行结果:
带 p 的含义就是不用带路径,系统会自动搜索你要执行的程序,不带 p 则相反。所以你要执行哪个程序,背后的含义是 a) 你在哪 b) 你是谁。可见 execlp 就是 b,execl 就是 ab。当然这里的搜索默认只有系统的命令才能找到,如果需要执行自己的命令,需要提前把自己的命令与 PATH 关联。
✔ 测试用例四:
所以 execvp 无非就是不带路径,使用指针数组传参。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
char\* const my_argv[] = {"ls", "-l", "-a", "-i", NULL};
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
execvp("ls", my_argv);
exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
}
else
{
printf("wait error!\n");
}
return 0;
}
💨运行结果:
✔ 测试用例五:
e 表示传入默认的或者自定义的环境变量给目标可执行程序。
子进程跑自己的程序 mycmd.c:
makefile 里需要 make 时一次生成 2 份可执行程序。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(int argc, char\* argv[], char\* env[])
{
pid_t id = fork();
if(id == 0)
{
char\* const my_env[] = {"MYENV=hellobit!", NULL};
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
//测试1
//execle("./mycmd", "mycmd", NULL, my\_env);
//测试2
//char\* const my\_argv[] = {"mycmd", NULL};
//execve("./mycmd", my\_argv, my\_env);
//测试3
//char\* const my\_argv[] = {"mycmd", NULL};
//execve("./mycmd", my\_argv, env);//main函数所继承下来的环境变量也可以传递给./mycmd
//测试4
//char\* const my\_argv[] = {"mycmd", NULL};
//execvpe("mycmd", my\_argv, my\_env);
//测试4
execle("./mycmd", "mycmd", NULL, env);
exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
}
else
{
printf("wait error!\n");
}
return 0;
}
💨运行结果:
- 测试 1
- 测试 2
- 测试 3
- 测试 4
execvpe 并没有替换成功,因为它带了 p,所以它会在环境变量里查找目标程序,但是此时目标程序就在当前路径。所以目前的场景是不适合使用带 p 的函数的。如果想让 execvpe 查找到目标程序,就只能将当前路径添加到环境变量中,或将目标程序添加到任意环境变量的路径下。
- 测试 4
main 函数可以获得环境变量,环境变量再传给子进程。所以现在我们就能理解环境变量是怎么被子进程继承的,本质是通过 exec 函数将环境变量传入的。
💦 命名理解
这些函数原型看起来很容易混淆,但只要掌握了规律就很好记。
- l(list),表示参数采用列表。
- v(vector),表示参数使用数组。
- p(path),自动搜索环境变量 PATH。
- e(env),表示自己维护环境变量。
💦 简单模拟shell解释器
子进程执行新程序的需求 ❓
在之前,我们举过 1 个例子:你是村长家的儿子,是程序员,你不擅长和女生打交道,所以你去通过王婆去找如花表达你的爱意,村里人都知道如花已经心有所属了,而你又是村长家的儿子。王婆心想,这趟浑水我可不不趟,万一搞砸了,可能会影响到自己以后的职业发展,但又碍于你是村长家的儿子,不敢得罪。所以,机智的王婆说:呀!最近的活太多了,这样吧,我给你找我们公司的销冠(其实是实习生,比较好欺负),你自己跟销冠对接。就算谈不成,王婆也可以周旋(再给你找业务好的实习生)。王婆会根据工作的难易程度,简单的自己做,复杂的交给其它人。
这里你是用户;王婆是命令行解释器中的 bash;销冠(实习生)是子进程;如花是操作系统;销冠(实习生)谈砸了,不影响王婆就如子进程崩了不会影响父进程。
✔ 测试用例一:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define NUM 128
#define SIZE 32
char command_line[NUM];
char\* command_parse[SIZE];
int main()
{
while(1)
{
//1.获取命令
memset(command_line, '\0', sizeof(command_line));
printf("[DanceBit@myhost myshell]$ ");
fflush(stdout);
if(fgets(command_line, NUM - 1, stdin))
{
command_line[strlen(command_line) - 1] = '\0';//\n = '\0'
//2.加工命令
int index = 0;
command_parse[index] = strtok(command_line, " ");//strtok以空格分割"ls -a -l"于指针数组,strtok可以适用于"ls -a -l"。
while(1)
{
index++;
command_parse[index] = strtok(NULL, " ");
if(command_parse[index] == NULL)
{
break;
}
}
//3.执行非内置命令
if(fork() == 0)
{
//child
execvp(command_parse[0], command_parse);//我们选择v、p,因为有数组,且执行非内置命令
exit(1);//父进程拿到1,说明execvp替换失败。
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
if(ret > 0 && WIFEXITED(status))
{
//等待成功且子进程正常退出
printf("Exit Code: %d\n", WEXITSTATUS(status));
}
}
}
return 0;
}
💨运行结果:
可以看到我们自己模拟的 shell 可以支持大部分命令,但有部分命令是不支持的,如 ll、>、| 。不支持的原因也很好理解,重定向和管道是需要我们理解了它的原理,然后才能实现的,后面我们再对 myshell 进行完善。
myshell 和 mini_shell 中使用的 echo 是同一个 echo 吗 ❓
不一定,如果感觉有些抽象的话,可以这么理解:有些命令实际让子进程去运行,子进程是不影响父进程的,此时有可能就会出现一些奇怪的现象,如:
最全的Linux教程,Linux从入门到精通
======================
-
linux从入门到精通(第2版)
-
Linux系统移植
-
Linux驱动开发入门与实战
-
LINUX 系统移植 第2版
-
Linux开源网络全栈详解 从DPDK到OpenFlow
第一份《Linux从入门到精通》466页
====================
内容简介
====
本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。
本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。
需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
全的Linux教程,Linux从入门到精通
======================
-
linux从入门到精通(第2版)
-
Linux系统移植
-
Linux驱动开发入门与实战
-
LINUX 系统移植 第2版
-
Linux开源网络全栈详解 从DPDK到OpenFlow
第一份《Linux从入门到精通》466页
====================
内容简介
====
本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。
本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。
需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!