进程的销毁
1.释放资源
2.记账信息
3.将进程状态设置成僵尸状态
4.转存储调度
进程终止的方法
正常退出
1.exit (C库函数) exit函数要先执行一些清除操作,然后才将控制权交给内核
a.用户执行atexit或者onexit定义的清理函数;
b.关闭所有的流,所有的缓存数据均被写入;
c.调用_exit函数。
2.main函数退出
3._exit() (操作系统提供的API,进程终止函数) _exit函数执行后会立即返回给内核
异常退出
1.ctrl + c
2.assert()
3.abort()
4.信号终止(ps:段错误、栈溢出)
"_NR"是在Linux的源码中为每个系统调用加上的前缀
_exit终止调用进程,但不关闭文件,不清除输出缓存,也不调用出口函数。
_exit()定义在unistd.h中,直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构。
exit()函数定义在stdlib.h中,在这些基础上作了一些包装,在执行退出之前加了若干道工序.
exit()函数与_exit()函数最大的区别就在于exit()函数在调用_exit系统调用之前要检查文件的打开情况,
把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。 刷新缓存
实际上,main函数执行完毕后,OS会注册一个清理函数并交给用户执行,其调用了atexit函数或者onexit函数
atexit()回调函数 最多能注册32个,后注册先执行,类似于栈。
打开一个文件就是打开一个流,在执行exit函数退出的时候,实际上会把文件的内容刷新到缓冲区,
并且关闭流,最后执行_exit函数。
尽量使用_exit函数
return是一种常见的退出进程的方法,执行return n相当于exit(n),因为会把main函数的返回值当做exit函数的参数传入。
echo $?可以查看程序上一次退出的状态码 退出码的范围 0-255
进程等待:回收僵尸子进程
wait
pid_t wait(int *status);
正常退出返回值为被回收的子进程ID
错误返回-1。
8-15位是子进程的退出码
一直等待子进程结束,才将返回结果反馈给wait。
如果传递NULL,表示不关心子进程的退出状态信息。
一次等待只能结束一个子进程。
注意:wait必须和子进程的个数相匹配。(eg:接儿子上下学)
waitpid
pid_t waitpid(pid_t pid, int * status, int options)
status 子进程的退出码
举例:接指定的儿子(指定pid)上下学
参数pid
>0 等待与其PID相等的子进程死亡
=0 等待本组中的任何一个子进程死亡 fork创建的父子进程属于同一个组
=-1 等待当前进程中的任何一个子进程死亡
<-1 等待其组ID等于pid的绝对值|pid|的任一子进程死亡
status
可以按照位图去理解,是个输出型参数,
正常终止时,0-7位是0,8-15位表示退出的状态,即就是退出状态码。
异常终止时,实际上就是被信号所终止,0-7位是终止信号 core dump标志,8-15位没有用。
option选项 一般写0 实际上也是个位图
WNOHANG
非阻塞型等待
轮询
如果有子进程结束,则回收并返回子进程ID(ret > 0),如果此时没有子进程需要等待那么ret < 0
如果子进程还没有结束,那么ret == 0
WIFEXITED(status)
如果正常退出,返回真
WEXITSTATUS(status)
返回子进程的退出码
WIFSIGNALED(status)
如果被信号干掉,则返回真
WTERMSIG(status)
获得搞死该进程的信号值
头文件的访问顺序
系统头文件->库函数头文件->自定义头文件
注意:只要PCB不被删掉,那么进程就一直存在。进程替换后,其原来进程后面的内容将不会被执行。
进程替换
fork出子进程后,希望子进程运行和父进程(ps:比如运行a.out)不一样的进程(例如运行磁盘上的ls),这就需要进程替换了。
一开始,虚拟内存通过三级映射(虚拟内存->页目录->页表)与物理内存建立连接,这时候如果想要运行磁盘上的ls进程,就需要调用
exec系列函数,然后PCB不变,干掉虚拟内存通过三级映射与物理内存建立的连接,里面的代码段以及数据段都没有了,
再将ls的代码段以及数据段放入物理内存的某个空间,然后再建立三级映射,
通过readelf -h a.out可以看到程序执行的起始(入口)地址,这时候替换掉eip的值(改为ls的入口地址),那么就可以
找到ls的入口地址,接下来运行ls这个进程,然后栈和堆需要重新开始(之前的函数栈帧并不能保证找到下个指令的开始地址),
最终达到替换进程的目的。
实际上,操作系统在创建PCB的同时,会创建一个加载器,加载器会解析文件格式,并将解析的结果存入物理内存中的代码段以及数据段中。
加载器:也叫作程序加载器,主要用于加载程序和库,它负责将程序送入内存,为程序的运行提供准备,一旦加载完成,
OS才会把控制权移交给执行代码的程序。
Unix: loader(加载器)是系统调用(execve)的句柄。
a.out是个ELF文件,通过readelf -h a.out可以看到程序执行的起始(入口)地址。可以看到这个字段Entry point address
exec系列函数
下面这些函数如果调用成功,则加载新的程序从启动代码开始执行,不再返回,如果失败则返回-1,成功则没有返回值。
如果执行exec系列函数后面还有别的语句,那么替换进程成功后,就不会有返回值。
list
int execl(const char *path, const char *arg, ...); //必须指明路径
execl("/bin/ls", "/bin/ls", "-l", "-t", NULL);
execl("./hello", "./hello", NULL);
execl("/bin/bash", "ps", "-ef", NULL);
int execlp(const char *file, const char *arg, ...);
execlp("ls", "ls", "-l", "-t", NULL);
int execle(const char *path, const char *arg, ...,char *const envp[]);
需要提前将envp传入指针数组中。
execl("./hello", "./hello", NULL, envp);
path绝对路径或者相对路径 envp环境变量
vector 向量
char *argv[] = {"/bin/ls", "-l", NULL};
int execv(const char *path, char *const argv[]);
execv("/bin/ls", argv);
int execvp(const char *file, char *const argv[]);
execvp("ls", argv); //实际上exec系列函数只是将参数传进去,并不会进行校验,因此直接替换掉argv[0]的值。
int execve(const char *path, char *const argv[], char *const envp[]); //系统调用
execve("./h", argv, env);
file可执行文件名 argv(main函数的命令行参数)
其余5个都是C库函数
l(list) 可变参数列表
v(vector) 数组
p(path) 自动搜索已存在的环境变量
e(env) 需要自己维护的环境变量
模拟system
/bin/sh -c "ls -l"
if (0 == pid)
{
char *argv[] = {
"sh", "-c", cmd, NULL
};
execvp("/bin/sh", argv);
exit(127);
}
else
{
int status ;
waitpid(pid, &status, 0);
if ( WIFEXITED(status) )
{
ret = WEXITSTATUS(status);
}
else
{
ret = -1;
}
}
find / -name ls 2>/dev/null 查找ls的位置,并且过滤掉打印的错误信息
这里2表示strerr,也就是文件描述符中的标准错误对应的返回值,/dev/null是linux中的一个特殊文件,相当于一个垃圾桶。
它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个EOF。通常被用于丢弃不需要的数据输出。
清空文件内容:
1. echo "" > test.txt 文件大小被截断成1个字节 echo > test.txt
2. > test.txt
3. cat /dev/null > test.txt
4. cp /dev/null test.txt
5. dd if=/dev/null of=test.txt
6. truncate -s 0 test.txt truncate用来将一个文件缩小或者扩展到给定大小 -s来指定文件大小
/dev/zero实际上产生连续不断的null的流(二进制的零流,而不是ASCII型的)。写入它的输出会丢失不见,
/dev/zero主要的用处是用来创建一个指定长度用于初始化的空文件,像临时交换文件。
为特定的目的而用零去填充一个指定大小的文件, 如挂载一个文件系统到环回设备。
该设备无穷尽地提供0,可以使用任何你需要的数目——设备提供的要多的多。他可以用于向设备或文件写入字符串0。
bash
首先bash可以看成是一个父进程,那么输入ls命令时,实际上是从磁盘上获取到ls的代码加载到物理内存上,然后再运行ls进程,
这也就是进程替换,ls进程运行结束后,被bash进程回收,(实际上bash一直在等待ls进程执行结束)。
简易版的shell
1.从标准输入中读取字符串到内存中;
2.对用户输入进行解析,要执行的指令是什么,参数是什么;
参数开始
当前状态为参数结束状态,并且当前字符为非空格,此时进入参数开始状态,并且把当前指针保存在一个数组中。
参数结束
当前状态为参数开始状态,并且当前字符为空格,此时进入参数结束状态,并且把当前指针指向的字符改为'\0'。
strtok
ll是个别名
3.创建子进程fork
a.子进程进行父进程的程序替换execvp;
b.父进程进行进程等待wait。
4.当子进程执行完毕后从wait中返回,继续下一次循环。
关于myshell中cd不生效:
执行cd ..时,myshell又会创建一个子进程,实际上子进程对cd进行了程序替换,
但是由于子进程执行结束后替换的目录就被销毁了,然而myshell的目录仍是之前的目录,
因此看不到切换目录。
如果发现输入的指令是cd,直接调用chdir函数修改进程自身的工作目录。
1.普通命令:shell创建子进程进行程序替换来实现;
2.内建指令:shell进程自身判定输入的指令后,调用相关系统函数实现,本质上是对shell进程自身来操作。
myshell不支持
1.alias
2.内建指令(比如cd)
3.管道
4.输出重定向
5.命令提示符中不能显示用户名,主机名,当前目录