@TOC
目录
一.冯诺依曼体系结构
二. 操作系统(Operator System)
概念
设计OS的目的
定位
总结
系统调用和库函数概念
进程
基本概念
描述进程-PCB
task_struct-PCB的一种
task_ struct内容分类
组织进程
查看进程
通过系统调用获取进程标识符
进程状态
D--深度睡眠状态
Z--僵尸进程
孤儿进程
特点
进程的竞争性
进程的优先级
进程独立性
并行
并发
环境变量
添加环境变量,删除环境变量
添加环境变量
删除环境变量
查看环境变量
PATH
给PATH中添加可执行程序
C/C++获取环境变量
main函数的参数
使用第三个参数获取环境变量
environ外部导入环境变量
关于本地变量的说明
进程控制
进程创建
fork( )
fork啥时候会出错
写时拷贝
进程终止
程序退出码
strerror
程序退出的几种状态
终止的常见做法
exit和_exit
终止的时候,内核做了什么?
进程等待
如何等待
wait
status
waitpid
信号终止
库里面提供的宏
阻塞等待和非阻塞等待
阻塞等待
非阻塞等待
进程替换
原理
如何替换
execl
子进程替换
execv
execlp
execvp
execle/execvpe/execve
函数命名总结
简易shell搭建
基本框架的搭建
ls
cd
export
env
echo
总体代码:
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成
关于冯诺依曼,必须强调几点:
一句话,所有设备都只能直接和内存打交道。
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
与硬件交互,管理所有的软硬件资源
为用户程序(应用程序)提供一个良好的执行环境
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
计算机管理硬件
1.先描述,用struct结构体
2.再组织,用链表或者其他高效的数据结构
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
我们在Linux环境中创建一个hello.c的C语言文件,然后运行编译生成的hello文件,调用一下命令:
ps ajx | head -1 && ps ajx | grep hello
也可以在/proc中查询:
ls ./proc
进程创建函数fork()
下面是用fork()创建子进程的函数
#include
#include
#include
int main()
{
int ret = fork();
if (ret < 0) {
perror("fork");
return 1;
}
else if (ret == 0) { //child
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}
else { //father
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
这个R状态比较特殊,请看下面代码:
#include
#include
int main()
{
while(1)
{
printf("my pid is : %d\n",getpid());
}
return 0;
}
明明是一个不停在循环的代码,按理来说肯定在运行状态,应该是R,怎么是S?
打个比方:我们使用cout往显示屏中打印字符串,这里的输出设备就是我们的屏幕,但是输入输出设备是很慢的相对于CPU来说,可能当输入输出一个字符时CPU就已经跑了上百万行代码了。所以当我们往显示屏打印字符串时,操作系统并不会直接将该进程直接运行,而是等到当某种资源就绪后会将该进程链接到运行队列中,运行是很快的事,但是等待时间就比较长,在查看进程状态,一个死循环,绝大部分时候都是S,某个瞬间会变成R。
如何才能让进程处于运行态呢?很简单,我们写一个不和外设交互的死循环即可
#include
#include
#include
int main()
{
while(1){
int a = 0;
}
return 0;
}
那后面的+号是什么意思呢?
状态码上带有+
号,代表进程是一个前台进程
CTRL+C
终止的都是前台进程我们可以使用kill -9
干掉该进程,干掉进程之后,就可以使用CTRL+C
恢复正常的命令行了
。
我们还可以使用命令来改变进程的状态
kill -l
其中我们要用到的是第19和第18,分别用于暂停/恢复一个进程
kill -19 PID //暂停PID这个进程
kill -18 PID //恢复PID这个进程
kill -9 PID //杀掉PID这个进程
除了基本的S状态,linux下还有一个专门的disk sleep状态。如同它的名字一样,这个状态是专门为访问硬盘的进程设计的
假设有下面这样一个场景
出现这种数据丢失,谁都不想的嘛。所以Linux就设置了一个D状态,
处于D状态的进程不能被操作系统kill掉。要想杀掉一个D状态的进程,只有下面三种办法
子进程只输出了一遍就退出了
原来是在输出第一遍之后就变成了僵尸进程。
不难发现子进程已经处于僵尸状态了,那这样子进程不就没法回收了吗?该进程就会一直占有CPU资源,那不就造成了内存泄露了吗,对的。另外僵尸进程是不能够用命令杀掉的(因为已经退出)。
我们总结下僵尸进程的危害:
父进程如果提前退出,那么子进程后退出,进入 Z 之后,那该如何处理呢?
父进程先退出,子进程就称之为 “ 孤儿进程 ”。孤儿进程被 1 号 init 进程领养 。
上面我们提到了子进程先退出就是僵尸进程,那么父进程先退出呢?
我们想想,此时父进程会是僵尸状态吗?
答案是不会的,父进程在此时会被他自己的父进程(bash)回收,而它的子进程则会交给1号进程领养,我们可以修改一下代码,让父进程先退出,然后运行:
这时子进程已经被1号进程给领养了。而且观察孤儿进程的状态是S,这样就不能使用Ctrl + c
系统进程的数目较多。而CPU资源等其他资源不够用,所以进程之间存在竞争性,也就出现了优先级,这里我们就需要提到一个概念,进程的优先级
我们知道系统进程众多,不加以约束必然会在运行队列中堵塞, 在运行过程中要想顺利地进行,必须要给他们进行先后排序。
进程在排队获取资源的本质就是在确认优先级。这是因为系统的某些慢资源不够多个进程同时使用,这时候就需要让进程进入排队来先后访问。
优先级越高,操作系统执行他的响应就越快。
可以用下面的命令来查看当前目录下的进程
ps -la
其中的 PRI
和 NI
就是我们进程优先级的数据
PRI
值越低,优先级越高NI
值是进程优先级的修正数据,我们修改进程优先级,修改的是NI
值而不是PRI
这两个值是有范围限制的,LInux并不支持用户无节制的修改优先级
我们可以使用top命令来进行优先级的修改
linux下修改优先级的操作如下,运行 hello 程序后,先查看它的优先级信息
在使用sudo top
后,进入界面按r
,输入需要设置的进程PID后,再输入需要调整的NI值
这里可以看到,hello 进程的优先级已经被我们改成了70
再来尝试第二次,这次NI设置为20
pid设置成20之后,为啥NI
值变成了19,而PRI
变成了99呢?
依据我们以往的惯性思维,既然进程优先级= PRI + NI,那么修改了之后不应该是原本的70+20=90吗?为什么是99呢?
这是因为每一次设置的时候,PRI都会被重置成80。所以可以直接记住,Linux下进程的优先级=80+Ni值
进程运行具有独立性,不会因为某个进程出错,而影响其他进程的运行
我们知道,一个进程是内核结构task_truck+代码和数据
组成的。而linux系统是通过进程地址空间
方式来保证进程的独立性
并行:多个进程在多个CPU下分割,同时运行
我们一般的电脑都是只有1个cpu,那是怎么做到多个进程运行的?
注意:多个进程都在系统中运行≠多个进程在系统中同时运行。要想知道这是怎么做到的,需要了解并发的概念
大部分操作系统都是分时的,操作系统会给每一个进程赋予一个时间片,这样在一个调度周期中,可以调用到每一个需要运行的进程。
这样,在一个时间段内,多个进程会通过交叉运行的方式,让每一个进程的代码,在这段时间内都能得到运一行
比如每一个进程运行10ms,假设有10个进程需要运行,那么在1s内,这10个进程都会被运行10次。1s=1000ms
cpu进行多个进程的快速交替运行,以实现我们看到的单cpu运行多个进程的情况
这种情况就叫做并发
当我们运行自己编译的一个可执行文件的时候,需要带上./指定路径
使用file命令查看系统的关键指令,我会发现它们和我们自己写的hello本质是一样的,都是一个
executable的可执行文件
那为啥运行ls pwd gcc等等系统命令的时间,不需要在前面带上./路径来运行呢?
因为:指向一个可执行程序,前提是需要找到他们。
Linux系统只能找到它自己预设好的命令,找不到我自己得程序
在linux命令行中,输入env
即可查看当前系统的环境变量
其中PATH
就是可执行程序存放的路径!系统就是通过环境变量来查找可执行程序的
bash命令行里面就可以定义变量,变量分为两种类型
直接使用变量名 = 值 的方式,就可以定义一个本地变量。使用echo
命令可以查看这个本地变量。这时候我们用 env | grep变量名
在环境变量里面查找,会发现当前的环境变量里面没有这个东西
这时候需要用export命令,创建一个环境变量
或者可以导入当前的本地变量
删除的时候则使用unset
命令取消环境变量
echo 环境变量名 //查看环境变量
set | less //查看所有的shell变量和环境变量
需要注意的是,系统预载的环境变量都是在配置文件里面的。当前我们对环境变量做的任何操作都只会临时保存。关闭当前的命令行重新开一个,之前设置的环境变量就会消失
系统的环境变量配置文件为/etc/bashrc,用户的则为工作目录下的.bashrc以及.bash_profile
使用echo $PATH
查看当前系统可执行文件的路径
这里的路径都以:
作为分割,linux查找命令的时候,就会在下面的这些路径里面查找
除了直接调用ls,我们还可以使用路径来调用ls
而如果想让系统能找到自己的可执行程序,就可以直接把可执行程序复制到这些路径中!
cp 可执行文件名 ~/bin
可执行文件名
比如现在,我把mytest这个可执行程序复制到了~/bin
也就是比如现在,我把mytest这个可执行程序复制到了~/bin
也就是/home/muxue/bin
的路径下,此时直接使用mytest就能找到对应的命令了!的路径下,此时直接使用mytest就能找到对应的命令了!
除了这种办法以外,我们还可以把当前的路径写入PATH
环境变量中
export PATH=$PATH:/path/to/executable
这个命令通过使用export命令将新路径添加到PATH中。$PATH表示已经存在的PATH路径,:/path/to/executable
则是要添加的新路径。注意,要使用冒号将新路径与已有路径分隔开来。
我们设置的这个环境变量都是临时的,所以重启了之后,自己设置的这个路径也会消失。一般情况下不建议在linux系统路径中安装自己的可执行程序,因为这样会污染系统的命令环境!
#include
//第一个参数指代命令个数,执行该可执行文件时传入的几个命令
//第二个参数是一个指针数组,存放了每一个命令的常量字符串
int main(int arg,char* argv[])
{
printf("arg: %d\n",arg);
for(int i =0;i
除了上面提到的main函数前两个参数,实际上main函数还可以带第三个参数!
//第一个参数指代命令个数,执行该可执行文件时传入的几个命令
//第二个参数是一个指针数组,存放了每一个命令的常量字符串
//第三个参数用于导入环境变量!
int main(int arg,char* argv[],char *envs[])
{
for(int i =0;envs[i];i++)
{
printf("envs[%d]: %s\n",i,envs[i]);
}
return 0;
}
因为envs是一个指针数组,所以终止for循环的条件就是envs[ i ] = NULL
除了上面这个办法,我们还可以用下面两种方式来获取环境变量
C语言提供了一个environ来导入环境变量,其作用和main函数第三个参数是一样的
extern char ** environ;
printf("get env from [environ]\n");
for(int i = 0; environ[i]; i++)
{
printf("%d: %s\n", i, environ[i]);
}
上面都是数组和指针,有个函数也可以获取环境变量
通过这个函数,我们可以写一个只能我自己这个用户能运行的可执行程序
int main(int arg,char* argv[],char *envs[])
{
char* user = getenv("USER");
if(strcasecmp(user,"pmb")!=0)//strcasecmp忽略大小写
{
printf("权限禁止!\n");
return -1;
}
printf("成功执行!\n");
return 0;
}
所谓的本地变量,其实是bash内部定义的变量。
我们首先需要了解的是,linux下大部分的进程或命令都是以子进程方式运行的,其父进程都是当前打开的bash
由此可知,bash内部的本地变量,并不会被这些子进程所继承
而环境变量具有全局属性,可以被子进程继承并获取!
那么问题来了,export/echo也是命令。如果它们也是子进程,那它们是怎么获取到bash内部的本地变量,并将其导入到环境变量中的呢?
实际上,这两个命令都是由bash自己执行的(调用自己的对应的函数完成功能)我们把这种命令称作内建命令。关于内建命令,我会在进程控制阶段讲解。
fork是一个pid_t类型的函数,它的返回值有两个:子进程返回0,父进程返回子进程pid,出错返回-1.
注:pid_t其实就是 int 类型
当一个进程调用fork函数的时候
mm_struct+页表
和内核数据结构task_strcut
给子进程fork
返回,开始调度器调度简单说来,便是fork
之前只有父进程单独运行。fork之后父子进程的执行流会分别执行,且相互独立。
fork之后,是父进程先执行还是子进程先执行依赖于调度器的调度。并非一定是父进程先执行!
需要注意的是,子进程虽然共享父进程的所有代码,但是它只能从fork之后开始执行
这里涉及到了cpu的eip程序计数器(又称pc指针)这玩意的作用就是保存当前正在执行的指令的下一条指令!
注意,这里说的是CPU执行的指令,并非linux下bash里面的命令
eip程序计数器会把下一个指令拷贝给子进程,子进程就会从该eip所指向的代码处(即fork之后的代码)开始运行
如果你写一个循环代码一直创建子进程,那么就有可能创建失败!
能够创建的子进程个数依赖于代码的复杂度
在父进程创建子进程的时候,父进程和子进程中的变量是指向相同的物理地址,但是在父进程或者子进程里面的变量值发生改变时,操作系统就会进行写时拷贝,这时候父进程和子进程的变量的物理地址就不同了(不过虚拟地址是一样的,只是虚拟地址和物理地址的映射不一样了)。
为什么要写时拷贝,创建子进程的时候直接把数据分开不行吗
答,这样会存在内存浪费!
一般情况下,父进程创建子进程之后,会出现下面的一些情况
所以最终linux采用了写时拷贝的方式,只会在需要的时候,拷贝父子需要修改的数据。这样延迟拷贝,变相提高了内存的使用率
相信每个初学C语言的人,都在想main函数结尾return 0到底有什么用?
这个返回值是给操作系统的,有操作系统来接收。
使用echo $?
命令查看环境变量,可以看到我们进程的退出码
#include
int main()
{
return 0;
}
如果我们把return 0 改成return 10 那么echo $?出来的结果就是 10
注:?
环境变量存放的是上一次运行的程序的退出码
比如这里我们连续两次访问这个环境变量,可以看到第一次的结果是我们自己运行的程序返回的10,第二次的结果是0(echo命令的返回值)
这里我们使用for循环打印一下库函数中strerrror
函数内记录的错误码
#include
#include
int main()
{
int i=0;
for(i=0;i<100;i++)
{
printf("[%d] %s\n",i,strerror(i));
}
return 0;
}
进一步加大循环的次数,能看到C语言中定义的错误码一共是134
个。后续全部打印unknown error
我们设计程序的退出码的时候,可以参照C语言库函数的错误码来进行设置,这样能更好地和库内部进行对接,或用strerror
函数来获取到错误信息
这就是用错误码来实现的异常管理
一般情况下,程序有下面的几种退出状态:
一般情况下,我们不会去在乎一个进程为何会成功;而更在乎一个错误的进程到底哪儿有bug。所以就需要想办法获取到这个进程的错误码
错误码表征了程序退出的信息,交由父进程进行读取
上面我们在bash中能通过echo读取上一个进程的退出码,那是因为我们自己运行的可执行程序,其父进程就为当前的bash。bash接受了我们进程的退出码,放入到了环境变量中
一般情况下,我们可以在main
函数中return,或者在任何地方使用exit()
来终止程序
这里还需要提及另外一个版本的exit()
,即_exit
最可见的区别便是,exit会刷新缓冲区,而_exit
不会
调用test2函数
调用test3函数
什么都没有打印,注意printf里面不能使用\n,这个是行刷新,会把缓冲区里面的数据全部刷新出来,关于缓冲区在文件系统会讲到。
在_exit
的man手册中也能看到,该函数会立即干掉这个进程;而exit
还会做一些其他的操作
我们知道,进程=内核结构task/mm_struct等+进程代码、数据
操作系统可能并不会释放该进程的task_struct/mm_struct,而是留给下一个进程使用!
要知道,如果想使用一个结构体,就需要对它进行开空间和初始化操作。而在操作系统中,创建、终止进程是一个非常高频的操作。如果总是不断的创建内核结构再释放,其内存利用率就很低,而且拖慢系统运行速度。
这时候系统就会使用内核的数据结构缓冲池,又称slab分派器,来管理这些仍待使用的内核结构。当有新进程出现的时候,更新内核结构的信息,并将其插入到运行队列中
之前讲过子进程退出,父进程如果不管不顾,就会造成僵尸进程的问题,从而导致内存泄漏等一系列问题
kill -9
也无法杀掉这个进程所以父进程需要监看子进程的退出状态,并进行相应的操作
父进程通过进程等待的方式回收子进程资源,获取子进程的退出信息
进程等待这里我们需要用到两个函数
pid_t wait(int*status);
pid_t waitpid(pid_t pid, int *status, int options);
头文件分别是
#include
#include
wait函数作用是等待子进程退出,status是一个输出型参数,子进程退出后,我们可以从中获取到子进程的退出信息
#include
#include
#include
#include
#include
int main()
{
int test = 10;
pid_t ret = fork();
if(ret == 0)
{
int i = 5;
while(i--)
{
printf("我是子进程%-5d, ppid:%-5d, ret:%-5d, &ret:%p\n\n",getpid(),getppid(),ret,&ret);
sleep(1);
}
printf("子进程退出\n");
exit(0);
}
else
{
printf("我是父进程%-5d, ppid:%-5d, ret:%-5d, &ret:%p\n\n",getpid(),getppid(),ret,&ret);
sleep(5);
int status = 0;
pid_t st = wait(&status);
printf("等待成功,子进程pid:%d, 状态信息:%d\n",st,status);
sleep(5);
}
return 0;
}
我们成功获取了子进程的pid以及退出码0
那如果我们修改一下子进程中exit
的值呢?
exit(11);
状态信息为啥是2816,为啥不是11。
实际上,输出型参数中status
的值并非是完整的退出状态信息,其分为下面两种情况
所以说,正确访问状态码的方式,是先将status右移8位,再用按位与取出状态码
#include
#include
#include
#include
#include
int main()
{
int test = 10;
pid_t ret = fork();
if(ret == 0)
{
int i = 5;
while(i--)
{
printf("我是子进程%-5d, ppid:%-5d, ret:%-5d, &ret:%p\n",getpid(),getppid(),ret,&ret);
//i++;
sleep(1);
}
printf("子进程退出\n");
exit(11);
}
else
{
printf("我是父进程%-5d, ppid:%-5d, ret:%-5d, &ret:%p\n\n",getpid(),getppid(),ret,&ret);
sleep(5);
int status = 0;
pid_t st = wait(&status);
//printf("等待成功,子进程pid:%d, 状态信息:%d\n",st,status);//直接打印status是错误的!
//status的低16位才有效,其中这16位的高8位是状态码
printf("等待成功,子进程pid:%d, 状态信息:%d\n",st,(status>>8)&0xFF);//0xff是8个1
sleep(5);
}
return 0;
}
函数原型
pid_t waitpid(pid_t pid, int *status, int options);
>0
指定等待子进程pid;-1
等待所有子进程返回值:
waitpid
发现没有已退出的子进程可收集,返回0-1
。此时errno
会被设置成相对应的值来显示错误int main()
{
int test = 10;
pid_t ret = fork();
if(ret == 0)
{
int i = 4;
while(i--)
{
printf("我是子进程%-5d, ppid:%-5d, ret:%-5d, &ret:%p\n",getpid(),getppid(),ret,&ret);
//i++;
sleep(1);
}
printf("子进程退出\n");
exit(0);
}
else
{
printf("我是父进程%-5d, ppid:%-5d, ret:%-5d, &ret:%p\n\n",getpid(),getppid(),ret,&ret);
sleep(5);
int status = 0;
pid_t st = waitpid(ret,&status,0);//指定等待上面创建的子进程
//status的低16位才有效,其中这16位的高8位是状态码
printf("等待成功,子进程pid:%d, 状态信息:%d\n",st,(status>>8)&0xFF);//0xff是8个1
sleep(5);
}
return 0;
}
前面提到了,除了正常的终止,status中还可以保存信号终止的信息
这里的core dump
标志是用来干嘛的我们暂且不提,先来试试用kill来干掉子进程!
这里我们要取出的是status
中最低7位的数据,就需要按位与一个二进制末尾是7个1的数字
注意:如果子进程是因为信号退出,那么我们不需要关注退出码,其没有意义!
自己写按位与多麻烦呀,库里面提供了几个宏供我们使用
WIFEXITED(status)
查看子进程是否是正常退出的,正常退出为真WIFSIGNALED(status)
查看子进程是否为信号终止,信号终止返回真WEXITSTATUS(status)
提取子进程退出码WTERMSIG(status)
提取子进程退出信号//其余部分代码和上面相同,子进程exit(11)
int status = 0;
pid_t st = waitpid(ret,&status,0);//指定等待上面创建的子进程
if(WIFEXITED(status))//子进程正常退出返回真
{
printf("等待成功,子进程pid:%d, 状态:%d,信号:%d\n",st,WEXITSTATUS(status),WTERMSIG(status));
}
else
{
printf("非正常退出,子进程pid:%d, 状态:%d,信号:%d\n",st,WEXITSTATUS(status),WTERMSIG(status));
}
前面的waitpid
函数中的option
参数就和阻塞/非阻塞等待有关
当我们调用某些函数的时候,因为条件不就绪,需要我们进行阻塞等待
本质:当前程序自己变成阻塞状态,当一切就绪的时候再被唤醒。
这时候我们等待的不是硬件资源,而是等待子进程运行结束(软件资源)
阻塞等待时,将父进程放入子进程task_struct中的等待队列。当操作系统检测出子进程退出,就从等待队列中唤醒父进程,阻塞等待成功!
给waitpid
的option
传入0,即为阻塞等待
pid_t st = waitpid(-1,&status,0);//阻塞等待
在子进程被信号干掉或者执行完毕退出之前,父进程不会向后执行代码。在用户层面看来,就是一个程序卡住了
给
waitpid的
option传入WNOHANG
,即为非阻塞等待
等待期间,父进程可以干其他的事情
#include
#include
#include
#include
#include
int add(int a,int b){
return a+b;
}
int pls(int a,int b){
return a*b;
}
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int i =5;
while(i--)
{
printf("我是子进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
sleep(2);
}
exit(0);
}
else if(id >0)
{
// 父进程
// 基于非阻塞的轮询等待方案
int status = 0;
int i = 1, j=2;
while(1)
{
pid_t ret = waitpid(-1, &status, WNOHANG);
if(ret > 0)
{
printf("等待成功, %d, exit code: %d, exit sig: %d\n", ret, WIFEXITED(status), WTERMSIG(status));
break;
}
else if(ret == 0)
{
//等待成功了,但子进程没有退出
printf("子进程好了没?没有,父进程做其他事情\n");
printf("add %d ",add(i++,j++));
printf("pls %d\n",pls(i++,j++));
sleep(1);
}
else{
//err
printf("父进程等待出错!\n");
break;
}
}
}
return 0;
}
这里我们给父进程写了一个死循环,一直等待子进程退出。每一次循环都会调用一次waitpid
的接口,直到成功获取了子进程的退出信息
这种多次调用waitpid
接口的方式又被称为轮询检测
在之前的fork
中,我们的子进程都是运行的已经预先写好的代码,或者说是继承了父进程的代码继续向后执行。
进程替换
就是让子进程可以执行磁盘里面其他的可执行文件,包括Linux系统的命令、其他语言写的代码py c++ php
等等…
其实就是让子进程通过调用操作系统的接口,来执行一个已有的可执行程序
程序替换的过程
系统提供了非常多的函数接口,供我们在一个程序中调用系统中其他的可执行程序
要想调用,首先要找到这个程序在那儿,以及要用什么办法执行这个程序(命令行参数)下面以具体的例子来了解一下吧
需要注意的是:我们需要先用fork
创建子进程,再调用上面这些函数接口来使用其他可执行文件。这些函数接口本身并不会创建新的子进程!
int execl(const char *path, const char *arg, ...);
arg
代表需要执行的程序...
是可变参数,可以传入不定量的参数。这里我们填入的是命令行的参数#include
#include
#include
#include
int main()
{
printf("开始测试\n\n");
int ret = execl("/usr/bin/ls","ls","-l",NULL);
printf("执行结束 %d\n",ret);
return 0;
}
需要注意的是,当我们填入命令行参数的时候,必须要以NULL
作为参数的结尾
我们会发现,调用了其他可执行程序之后,在后面的printf函数并没有被执行!
这是因为,当我们用这个函数来调用其他可执行程序,本质上已经把当前的代码和数据替换掉了!既然是替换,那么原本的printf("执行结束 %d\n",ret);肯定也不会执行
返回值问题
那execl不是有一个int类型的返回值吗?如果程序替换了之后不会执行后面的代码,那这个返回值还有什么用呢?
这个返回值只有出错的时候才会返回-1,同时会更新ERRNO
现在我们把执行文件改成usr/bin/
这个错误文件,那么就会调用失败,同时可以看到调用失败的原因是,我们没有权限去执行/usr/bin
我们在执行exec
这些替换函数的时候,其实没有必要去判断返回值。因为这些函数只有出错的时候,才会执行后面的代码!
errno
找出错误原因即可了解了替换程序的基本方法了之后,可以先来试试写一个父子进程
int add(int a,int b){
return a+b;
}
int pls(int a,int b){
return a*b;
}
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int i = 3;
while(i--)
{
printf("我是子进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
sleep(2);//便于观察
int ret = execl("/usr/local/bin/python3","python3","test.py",NULL);
printf("子进程执行出错: %d\n",ret);
printf("子进程错误原因: %s\n",strerror(errno));
exit(-1);
}
}
else if(id >0)
{
// 父进程
// 基于非阻塞的轮询等待方案
int status = 0;
int i = 1, j=2;
printf("我是父进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
while(1)
{
pid_t ret = waitpid(-1, &status, WNOHANG);
if(ret > 0)
{
printf("等待成功, %d, exit code: %d,exit status : %d ,exit sig: %d\n", ret, WIFEXITED(status), WEXITSTATUS(status), WTERMSIG(status));
break;
}
else if(ret == 0)
{
//等待成功了,但子进程没有退出
printf("子进程好了没?没有,父进程做其他事情\n");
printf("add %d ",add(i++,j++));
printf("pls %d\n",pls(i++,j++));
sleep(1);
}
else{
//err
printf("父进程等待出错!\n");
break;
}
}
}
return 0;
}
exit code : 1子进程正常退出,exit status : 0子进程退出正确,exit sig :0不是由信号终止的。
可以看到,子进程替换了python程序成功了之后,不会执行后面的printf
假如子进程出现错误,如果该错误被捕获,程序没有崩溃,而是正常退出,比如test.py里面的代码是这样的
print("hello world")
a = 10 / 0
代码虽然有错误,但是该错误会被捕获,程序不会崩溃,输出为
exit status: 1,说明子进程退出码出错。
同时我们也可以看到,子进程执行程序替换,是不会影响父进程的(进程具有独立性)
这是因为数据发生了写时拷贝,程序替换的时候可以理解为代码和数据都通过写时拷贝进行了父子的分离(注意分离的是代码和数据,并非父子关系!)
int execv(const char *path, char *const argv[]);
可以看到这个函数莫得可变参数,而是需要我们用一个指针数组来传入命令行参数!其余都是一样的!
复习一下,const修饰指针有下面两种形式
*
之前修饰,代表该指针指向对象的内容不能被修改(地址里的内容不能改)*
之后修饰,代表该指针指向的对象不能被修改(指向的地址不能改)void testExecv()
{
printf("开始测试\n\n");
char*const arg[]={
"ls",
"-l",
"-a",
NULL
};
int ret = execv("/usr/bin/ls",arg);
printf("执行结束: %d\n",ret);
printf("错误原因: %s\n",strerror(errno));
return ;
}
int execlp(const char *file, const char *arg, ...);
注意,这里参数的说明从path
变成了file
这个函数和execl
的区别在于,它会自己去系统环境变量的PATH
里面查找可执行程序
void testExeclp()
{
printf("开始测试\n\n");
int ret = execlp("python3","python3","test.py",NULL);
printf("执行结束: %d\n",ret);
printf("错误原因: %s\n",strerror(errno));
return ;
}
调用成功
随意指定一个程序,就会报错
int ret = execlp("python12","python3","test.py",NULL);
int execvp(const char *file, char *const argv[]);
知道了execv/excel
之间的区别,那么execvp/execlp
之间的区别也就很明显
同样也是只有传参的区别,其他的操作完全一样
void testExecvp()
{
printf("开始测试\n\n");
char*const arg[]={
"ls",
"-l",
"-a",
NULL
};
int ret = execvp("ls",arg);
printf("执行结束: %d\n",ret);
printf("错误原因: %s\n",strerror(errno));
return ;
}
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
int execve(const char *filename, char *const argv[],
char *const envp[]);
首先它们的函数名中都有个e,这个e代表的是环境变量,代表我们可以把特定的环境变量传入其中进行处理。它们的环境变量都是在最末尾传的
函数 | 参数 | 说明 |
execle | 可执行文件的完整路径,命令行参数,环境变量 | 利用可变参数传入命令行参数 |
execve | 可执行文件的完整路径,命令行参数,环境变量 | 利用数组传入命令行参数 |
execvpe | 可执行文件名字,命令行参数,环境变量 | 利用数组传入命令行参数;只需要传入可执行文件的名字,会自动在PATH 里面搜索 |
为了简便,命令行中的提示符我们可以直接用printf打印,而具体执行命令可以交给子进程去做,现在的关键是如何将获得的命令行中的命令切割。我们在学习C语言时提到了strtok函数,正好这个函数可以用来作为切割。
#define NUM 1024
#define SIZE 32
#define SEP " "
int Transfor(char commend[],char* arg[])
{
//用字符SEP来分割commend字符串,并将第一个标记存储到arg[0]中
arg[0]=strtok(commend,SEP);
if(arg[0]==NULL)
return 1;
int i=1;
//以NULL作为第一个参数,他会继续上次的操作,从上次标记的位置开始寻找下一个标记
while(arg[i++]=strtok(NULL,SEP));
return 0;
}
int main()
{
char myenv[MAX][MAX]={0};
while(1)
{
char commend[MAX];
char* arg[MAX_NUM];
printf("[lisi@VM-8-12-centos lesson11]$ ");
fflush(stdout);//立即刷新
char*str=fgets(commend,sizeof(commend),stdin);//从键盘中读入一行字符串存到commend中
if(str==0) continue;
commend[strlen(commend)-1]='\0';//键盘输入最后一个是回车'\n',改成'\0'
Transfor(commend,arg);
pid_t id=fork();
if(id==0)
{
//child
execvp(arg[0],arg);//进程替换,新程序的名字和参数都存在arg数组中,有我们的shell完成
}
//parent
int status=0;
pid_t pid=waitpid(id,&status,0);//进程等待父进程获取子进程的退出状态码
}
return 0;
}
if(strcmp(arg[0],"ls")==0)
{
int pos=0;
while(arg[pos]) pos++;
arg[pos++]=(char*)"--color=auto";//添加 --color=auto 参数,这会使 "ls" 命令在终端中显示彩色的输出,以区分不同类型的文件。
arg[pos]=NULL;//因为要调用execvp函数,所以数组最后一个数必然是NULL
}
else if(strcmp(arg[0],"cd")==0)
{
if(arg[1]!=NULL)
chdir(arg[1]);
//chdir 是一个用于改变当前工作目录的函数。
//int chdir(const char *path);
//path 参数是一个指向要切换到的目标目录的字符串指针。
//需要注意的是,chdir 只会在当前进程中更改当前工作目录,不会影响到父进程或其他 进程的当前工作目录。这意味着,即使在子进程中使用 chdir 更改了当前工作目录,在父进程中依然保持不 变。
continue;
}
//这段代码的功能是允许用户通过输入 "cd [目录路径]" 来更改当前工作目录。如果用户输入了 "cd" 命令,且命令后面跟着一个目标目录的路径,程序将尝试将当前工作目录更改为这个目标目录。
像上面这种让bash自己执行的任务我们称之为内建命令/内置命令
同样的我们导入环境变量时我们想的是将环境变量导给父进程,这样子进程也能够继承父进程的环境变量,但是如果我们只是单纯的将arg中的环境变量通过putenv导入的话会出现问题的,因为我们每次执行新的命令时arg中的内容都会发生改变,也就是其实arg是一个随时有可能被修改的指针数组,这时你导入的结果就可能会出现差错,解决方法是通过自己再定义一个专门存放环境变量的数组中,然后在putenv进去。 一般用户自定义的环境变量,在bash中要用户自己来进行维护,不要用一个经常被覆盖的缓冲区来保存环境变量。
else if(strcmp(arg[0],"export")==0)
{
if(arg[1]!=NULL)
{
strcpy(myenv[envi],arg[1]);
putenv(myenv[envi++]);
}
continue;
}
这个命令其实还是查看的是父进程的环境变量表,其实我们学过的大多数关于环境变量的命令几乎都是内建命令,为了演示效果更加明显我们可以自己封装一个打印函数
void showEnv()
{
extern char** environ;
int i=0;
for(;environ[i];i++) printf("%d:%s\n",i+1,environ[i]);
}
//下面代码在main函数中
else if(strcmp(arg[0],"env")==0)
{
showEnv();
continue;
}
else if(strcmp(arg[0],"echo")==0)
{
char* targrt_env=NULL;
if(arg[1][0]=='$')
{
if(arg[1][1]=='?')
{
printf("%d\n",last_exit);
continue;
}
else{
targrt_env=getenv(arg[1]+1);
}
if(targrt_env!=NULL)
printf("%s=%s\n",arg[1]+1,targrt_env);
}
continue;
}
#include
#include
#include
#include
#include
#include
#define MAX 1024
#define MAX_NUM 64
#define SEP " "
int Transfor(char commend[],char* arg[])
{
arg[0]=strtok(commend,SEP);
if(arg[0]==NULL)
return 1;
int i=1;
while(arg[i++]=strtok(NULL,SEP));
return 0;
}
void showEnv()
{
extern char** environ;
int i=0;
for(;environ[i];i++) printf("%d:%s\n",i+1,environ[i]);
}
int main()
{
int last_exit=0;
char myenv[MAX][MAX]={0};
int envi=0;
while(1)
{
char commend[MAX];
char* arg[MAX_NUM];
printf("[lisi@VM-8-12-centos lesson11]$ ");
fflush(stdout);
char*str=fgets(commend,sizeof(commend),stdin);
if(str==0) continue;
commend[strlen(commend)-1]='\0';
Transfor(commend,arg);
if(strcmp(arg[0],"ls")==0)
{
int pos=0;
while(arg[pos]) pos++;
arg[pos++]=(char*)"--color=auto";
arg[pos]=NULL;
}
else if(strcmp(arg[0],"cd")==0)
{
if(arg[1]!=NULL) chdir(arg[1]);
continue;
}
else if(strcmp(arg[0],"export")==0)
{
if(arg[1]!=NULL)
{
strcpy(myenv[envi],arg[1]);
putenv(myenv[envi++]);
}
continue;
}
else if(strcmp(arg[0],"env")==0)
{
showEnv();
continue;
}
else if(strcmp(arg[0],"echo")==0)
{
char* targrt_env=NULL;
if(arg[1][0]=='$')
{
if(arg[1][1]=='?')
{
printf("%d\n",last_exit);
continue;
}
else{
targrt_env=getenv(arg[1]+1);
}
if(targrt_env!=NULL)
printf("%s=%s\n",arg[1]+1,targrt_env);
}
continue;
}
pid_t id=fork();
if(id==0)
{
//child
execvp(arg[0],arg);
}
//parent
int status=0;
pid_t pid=waitpid(id,&status,0);
if(pid>0)
{
last_exit=WEXITSTATUS(status);
}
}
return 0;
}