前言
这次的主要内容就是进程的实操,主要是进程创建,进程终止,进程等待和进程程序替换,最后我们在手写一个简单的shell
1.进程创建
进程创建就是fork,所以我们就讲一些知识性的就可以了
首先在创建子进程的时候,父进程和子进程都指向同一块区域,而且都是只读的权限,因为因为为可写的权限,肯定就不是指向同一块的,而且先指向同一块,然后有写入的,在分开指向那个写入的,不要一开始就全部分开指向,这样很浪费空间
当我们修改内容之后就会变成这样,首先代码段肯定是只读的,因为原来数据子进程父进程都是只读的,然后你去修改了,但是不会马上报错,而是去判断一下,这里会触发系统错误,然后系统检测判断是什么错误(这个就是缺页中断)如果是这种指向同一块内容的,就会把子进程指向新的一块内容,这块内容是拷贝的,然后在修改,这就是写时拷贝
因为可能是gval++这种情况,这种情况就只能先拷贝,在修改,所以统一先拷贝在修改
2. 进程终止
2.1 错误码
这里增加一点,就是C++语言 makefile用g++,然后加上-std=c++11
main函数的返回值会返回给父进程或者系统(就是类似于bash进程这种)
然后echo $?可以查看上一个程序的返回值
修改一下名字,因为c++嘛
第二个echo就是返回的第一个echo的main函数返回值,因为指令也是C语言写的
一般返回0,表示成功,返回非0就表示错误,而且数字还代表着错误的原因
返回的这个就是错误码
这个就类似与当初的C语言中的错误码errno,对应的函数有perror(传入字符串,然后直接在传入的字符串后面添上错误信息,并一起打印),strerror(传入错误码,根据错误码打印出错误)
errno这个东西会记录上一个错误码
没有错误既就是0
接下来我们打印出所有的错误信息
这样我们就可以看出来了,错误码总共就只有那么130多个
这个错误码和上面的完全对应
但这个就和上面的错误码不对应了
因为上面我们打印的是C语言的错误码,系统的错误码,可以借鉴C语言的,当然也可以自己定错误码
2.2 终止进程
有三个方式
第一个就是return
第二个就是exit
第三个就是_exit
看这个我们就知道了,exit是终止程序的,里面的参数就是错误码,0为正常退出
其实_exit也是这样的,两个的使用没什么区别,稍微有一点区别就是缓冲区的问题
看这个我们就可以看出来了,exit会刷新缓冲区,然后在终止,但是_exit不会刷新缓冲区
这是为什么呢
看这个我们就知道了
exit是C语言的函数,底层会调用_exit系统接口,而_exit就是一个系统接口,这样我们还知道了,缓冲区不是在操作系统里面的,不然_exit也可以刷新缓冲区,所以我们就可以知道了,缓冲区是语言层面的,所以exit才能刷新缓冲区,C语言有C语言的缓冲区,c++也有自己的缓冲区
3. 进程等待
3.1 wait
我们再谈fork
fork如果创建子进程成功的话,那么会给子进程返回0,父进程返回子进程的pid
失败的话,没有子进程,然后返回给父进程-1,然后还会设置错误码
这个程序,子进程会先结束,然后进入僵尸状态等待父进程处理
怎么处理呢,就可以用到wait了
这个就是会在wait这个函数中等待,等待子进程结束,然后处理僵尸,才继续执行后面的状态
其中返回值就是子进程的pid,如果等待错误了就返回-1,然后那个status就是用来接收错误码数据的
这样的话,既可以可看到僵尸状态,然后又可以看到处理
因为处理就是一瞬间的事,如果不休眠10s的话,我们可能看不到僵尸了
看这个我们就知道了wait是可以处理僵尸的,而且子进程不结束,父进程如果运行到了wait那里,就会一直在那里等待,而且这个等待的是所以子进程
3.2 waitpid
接下来讲一下waitpid
这个有三个参数
第一个参数,如果你输入某个子进程的pid,那么就是指定等待某一个子进程,如果为-1的话,就是任意一个子进程
如果waitpid这样写的话,那么就和我们上面写的wait是一个效果
或者这样,因为这里的id就是子进程的
而第二个参数就是用来记录子进程的退出码的
但是我们打印的退出码却不是1,为什么呢,因为这个status其实不止记录了退出码,正常的退出有退出码,但是比如如果访问野指针,就直接异常终止了,这个的话,也是由status记录的,所以说status其实也是一个位图
int总共有32位,,其中
8~15就是记录退出码的
这样我们就可以通过位运算计算出退出码了
这样就可以了,先把status右移8位,然后&上0xFF,就只剩下那退出码的8位了
子进程访问野指针,1/0这种都是程序异常会终止的,怎么异常终止的呢,
有警告先不管
这里可以看出,子进程直接死掉了
这个就直接终止了
这个是怎么终止的呢,其实是系统在运行的过程中遇到了这个,然后产生了一个信号给进程,进程接收到了这个信号,就终止了
信号就是kill的信号
那个野指针就是11号信号
我们这样把子父进程都无限运行下去
这个1/0其实就是运算器溢出了,是8号信号
这个要打出信号,必须是父进程出错了,子进程被kill是不会打出信号的,父进程才会打出信号,因为子进程僵尸了
所以进程结束了
第一return 0----》这个是正常结束,结果正确
第二return 非0-----》结果不正确,有退出码
第三异常终止---》有退出信号
退出信号就在status的0~7位
第8位是一个core dump标志
因为0x7F是0111 1111
看这个,我们用8这个信号杀死,所以就是八号信号
或者我们直接遇到实际异常时也是这个信号
其实这个退出码和信号也是存在task_struct中的
还有就是我们还有其它方法获取退出码,没有必要用位运算,可以用系统提供的宏
这个WIFEXITED(status)是用来看是否正常终止,如果正常终止,则返回真,如果异常终止就是假
WEXITSTATUS(status)就是返回退出码的
3.3 实例
就是像一个vector中插入数据,每插入十个的话,就要创建一个子进程来保存到文件中 ,用这个来记录父进程数据的变化,然后就是子进程不会干扰父进程,就是保存失败也没事
这就可以了
这样也可以删掉那些文件了
一键删除
3.4 阻塞与非阻塞问题
上面这个代码,只能等待子进程结束了,才能继续自己的操作,这个就叫做阻塞
何谓非阻塞呢,就是说,父进程一边运行,一边判断,看等待好了没
但这个逻辑要靠我们实现
waitpid的第三个参数就是阻塞和非阻塞的东西
为0就表示阻塞,为WNOHANG就是非阻塞,这个就会判断那一瞬间
waitpid的返回值,>0就表示等待成功,而且返回的是子进程的pid
==0的话,就表示子进程还没有结束
<0就表示等待失败,比如我们第一个输入的pid根本不存在
这里我们把子进程休眠十秒
然后父进程一直判断,子进程结束没有,没有结束就干自己的事,干完了再来判断,一直这样,当等待成功的时候就可以退出来了
我们再把干自己的事完善一下
4. 进程程序替换
cp是复制一份的意思
底行模式这样写,可以替换
程序替换总共有七个
最后一个是系统调用接口,前面六个的底层实现都是调用了最后一个的
因为底层都是分装为命令行参数表的
4.1 execl
这样就实现程序替换了,一个进程可以执行别人的进程了
第一个参数是我们要执行的路径
后面的参数就是我们要执行写的格式,注意最后一个参数必须是nullptr,因为这些参数相当于是提供的命令行参数,命令行参数列表最后一个必须是nullptr
就这样,我们就可以执行我们自己的可执行程序了
这个是怎么程序替换的呢,其实运行到execl那里,程序就完全替换了
数据和内容都完全替换为execl里面的进程了,这也是写时拷贝
所以说execl后面的程序就不会运行了
虽然程序替换了,但是进程是没有改变的,没有重新创建新的进程,比如说进程的pid就没有变
这样我们就证明了,进程程序替换是不会改变进程的
如果execl替换成功,那么后面的程序就都被替换了,所以返回值也没有用了,所以execl的返回值只有在替换失败的时候才有用,用处就是判断失败了
看这个我们就知道了,替换失败返回的就是-1
这个把数据和代码从磁盘替换到内存的过程就是加载
就这样我们就可以实现子进程和父进程自己干自己不同的事了
4.2 python程序
就这样就运行了
看得出来,其实Python就是一个命令
4.3 脚本
4.4 execv
其实这个就是把原来的第二个参数以及后面的,包装在一个数组中,这个就是命令行参数的表,其实底层也是这样的
巧记
l表示list,所以你传参数的时候就要挨个挨个的像链表一样
v表示vector,就是要打包进去
4.5 execlp
l表示还是分开传
p的意思就是不用写路径,直接写命令就可以了
第一个表示你要运行谁,后面的就是我们写命令行的格式了,所以按理说,第一个和第二个参数是一样的
这个不写路径是怎么实现的呢,这个其实是默认去环境变量PATH去找的命令,所以能找到
4.6 execvp
4.7 execvpe
这个多了一个参数,表示可以传环境变量
当然我们也可以不传环境变量,因为子进程会继承父进程的,就算是程序替换,也是会继承父进程的环境变量的,因为环境变量具有全局属性嘛
,不传就是默认的,但我们如果传了呢
当为p的时候,第一个也可以传路径,这样就不是默认去PATH找了
如果这样传的话,那么子进程的环境变量就完全被传的覆盖了
这样我们就可以使用我们自己定义的环境变量了,因为第一个bash的环境变量也是这样来的
那如果我们不传全新的环境变量,我们要传原来的,然后新增一些怎么搞呢
getenv是获取环境变量,讲过的
putenv就可以新增环境变量了
最后那里就是增加的
当然这个子进程增加的环境变量肯定是不会影响其他进程的,是不会影响它的父进程的,不会影响bash,只会影响自己的子进程
4.8 补充
最后一点,execve是系统接口,其他的都是分装它的,都是C标准库分装的接口,只有execve这个才是真正的系统调用
总结
下一个对应的就是手写简单版shell