Linux操作进程

前言

这次的主要内容就是进程的实操,主要是进程创建,进程终止,进程等待和进程程序替换,最后我们在手写一个简单的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

相关推荐
a_安徒生9 分钟前
linux安装TDengine
linux·数据库·tdengine
追风赶月、16 分钟前
【Linux】线程概念与线程控制
linux·运维·服务器
小字节,大梦想18 分钟前
【Linux】重定向,dup
linux
CP-DD30 分钟前
Docker 容器化开发 应用
运维·docker·容器
blessing。。1 小时前
I2C学习
linux·单片机·嵌入式硬件·嵌入式
2202_754421542 小时前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
努力的悟空2 小时前
国土变更调查拓扑错误自动化修复工具的研究
运维·自动化
运维&陈同学2 小时前
【zookeeper03】消息队列与微服务之zookeeper集群部署
linux·微服务·zookeeper·云原生·消息队列·云计算·java-zookeeper
旦沐已成舟3 小时前
DevOps-Jenkins-新手入门级
服务器
周末不下雨3 小时前
win11+ubuntu22.04双系统 | 联想 24 y7000p | ubuntu 22.04 | 把ubuntu系统装到1T的移动固态硬盘上!!!
linux·运维·ubuntu