上一篇
操作系统
概念:操作系统内核+一堆应用 功能:管理计算机的软硬件资源
进程
程序:源代码经过编译产生的可执行文件,这个文件是静态的. 进程:程序运行起来的实例)是动态在运行的。
进程控制块
进程号(PID)
ps: 查看进程 ps aux ps -ef
getpid
例子:
#include <stdio.h>
#include <unistd.h>
int a = 10;
int main()
{
int a = 20;
printf("%d\n",a);
int c[1]={1};
printf("%d %d",c[2],getpid());
}
进程状态:就绪/运行/阻塞:从CPU的角度来理解
运行:进程占用CPU,并在CPU上运行;理解为进程正在使用cpu来执行自己的代码 就绪:进程已经具备运行条件,但是CPU还没有分配过来;理解为进程已经将运行前的准备工作全部做好了,就等着操作系统调用,占用CPI了. 阻塞:进程因等待某件事发生而暂时不能运行﹔例如等待IO输八,(调用某些阻塞接口
细分
进程状态:就绪/运行/阻塞细分的进程状态: ** R运行状态**(代码) 在就绪队列+运行状态的进程,stat都是R 处于R状态的进程,有可能在执行代码,有可能在运行队列(就绪队列)
S:可中断睡眠状态(代码) 进程正在睡眠(被阻塞〉,等待资源到来是唤醒,也可以通过其他进程信号或时钟中断唤醒,进入运行队列
D:不可中断睡眠状态:通常等待一个IO结束(也就是输入输出结束) T:暂停状态(ctrl+z)(代码) 结论:在linux下不要使用ctrl+z结束进程(fg恢复进程),不是结束,而是暂停(另外 ctrl+c中止进程,) t:跟踪状态: 调试程序的时候可以看到
X:死亡状态: 这个状态是用户看不到的,在PCB被内核释放的时候,进程会被置为X,紧接着进程就退出了 Z:僵尸状杰:
程序计数器
保存程序下一条执行的指令
上下文信息
保存寄存器当中的内容
内存指针
指向程序地址空间
记账信息
使用CPU时长,占用内存大小
IO信息
保存进程打开文件的信息
创建子进程
fork():创建出来一个子进程
头文件:#include <unistd.h>:后续系统调用函数都需要包含该头文件
fork()的返回值
创建成功 (fork会返回两次在父进程当中返回一次,在创建的子进程当中返回一次,子进程号:返回给父进程;==0返回给子进程 返回给子进程 创建失败 验证:代码( getppid)
例子
#include <stdio.h>
#include <unistd.h>
int main()
{
int res=fork();
printf("%d pid =%d ppid=%d",res,getpid(),getppid());
}
#include <stdio.h>
#include <unistd.h>
int main()
{
int res=fork();
printf("%d pid =%d ppid=%d\n",res,getpid(),getppid());
sleep(1);
}
原理
父进程成功创建的子进程是从fork()后面的代码开始执行的,原因要根据原理来说。
getppid
pid_t getppid(void) ; 谁调用,就获取谁的父进程的PID
普通进程父进程是bash
僵尸进程&僵尸状态
产生
进程先于父进程退出,子进程就会变成僵尸进程;、 模拟代码:
#include <stdio.h>
#include <unistd.h>
int main(){
pid_t ret = fork();
if(ret < 0){
return -1;
}else if(ret == 0){
printf("i am child process\n");
}else{
while(1){
printf("i am parent\n");
sleep(1);
}
}
return 0;
}
僵尸状态:Z
原因(重要)
子进程在退出的时候,会告知父进程(信号:后续会涉及到),父进程忽略处理,父进程并没有回收子进程的退出状态信息) 细节 1.子进程退出的时候,会告诉父进程信号 2.父进程也收到了来自子进程的通知" 3.父进程是忽略处理(不处理) 4.导致子进程的退出状态信息,一直没有得到回收 5.所以,子进程在内核当中的PCB就一直没有释放 6.从而就看到了子进程变成了僵尸进程(子进程的状态就是僵尸状态:Z)
僵尸进程的危害
子进程的PC没有被释放,导致内存泄漏。
解决方案:
杀死父进程:原因后续氏孤儿进程会涉及(不推荐,十>不重启探作系统:(不推荐厂 父进程进行进程等待J(后续讲>
孤儿进程-A
父进赀先于子进程退出,子进程就会变成孤儿进程
产生(模拟代码)
#include <stdio.h>
#include <unistd.h>
int main(){
pid_t ret = fork();
if(ret < 0){
return -1;
}else if(ret == 0){
while(1){
printf("i am child: pid: %d ppid: %d\n", getpid(), getppid());
sleep(1);
}
}else{
sleep(1);
printf("i am parent: %d\n", getpid());
}
return 0;
}
原因
父进程先于子进程退出之后,既定回收子进程的父进程不在了,所以子进程就变成孤儿了,所以称之为孤儿进程
孤儿进程的退出信息由谁回收呢
有进程号为1的寄养(/usr/lib/systemd/systend :操作系统启动的第一个进程,后续有很多进程都是由该进程创建出来的或者是由该进程的子孙创建出来的操作系统初始化的一些工作,操作系统就是一个软件(一堆代码))
孤儿进程有危害吗,有相应的状态
没有危害,没有孤儿状态
环境变量
什么是环境变量
环境变量是指在操作系统中里来指定操作系统运行的一些参数;换句话说,操作系统通过环境变量来找到运行时的一些资源例如:链接的时候,帮助链接容找到动态库(标准库的)﹔ 《用户自己编译的动态库,需要自己指定环境变量) 执行命令的时候,帮助用户找到该命令在哪一个位置,例如ls -a
常见的一些环境变量
PATH:指定可执行程序的搜索路径,程序员执行的命令之所以能找到,这个环境变量起到的作用〈汗马功劳) HOME:登录到linux操作系统的用户家目录 HOME=/home/wudu shell:当前的命令行解释器,默认是“/bin/bash” SHELL=/bin/bash
查看当前环境变量
env 环境变量名称:环境变量的值(使用:进行间隔) 1.系统当中的环境变量是有多个的 2.每一个环境变量的组织方式都是key(环境变量名称)=value(环境变量的值-可以拥有多个值,每个值之间都是使:进行间隔) PATH=aaa : bbb: ccc : ddd
在操作系统当中查找一个命令
which ls
echo $HOME
环境变量对应的文件
修改环境变量
(命令范式: export 环境变量名名称 =[$ 环境变量名称]:[新添加的环境变量的内容)
命令行中修改
特点:临时生效 如果是新增,可以不要「$环境变量名称],即export 环境变量名称=[新添加的环境变量的内容] 1.在当前的终端内生效,如果新打开终端,是找不到该环境变量的 2.当前命令行当中添加的环境变量,生命周期跟随当前终端。当前终端如果退出了,环境变量就没有了。
文件当中修改:
vim ~/.bash_profile
修改特点:修改完毕之后,不会理解生效,需要配合source[环境变量文件名称],永久生效:
新增:在文件末尾直接添加 :export 环境变量名称=[新添加的环境变量的内容];如果修改老的:在老的后面添加“:[新添加的环境变量的内容]”
如何让自己的程序,不需要加./
1.把我们的程序放到/usr/bin下面(不推荐大家使用的)(usr/bin中存放的是系统生成的可执行程序,用户不要在这里使用)
cp r2 /usr/bin
2.设置环境变量设置 在PATH环境变量当中增加可执行程序的路径(当搜索环境变量的时候,如果在某个路径下,已经找到可执行程序,则直接执行,不会在往后搜索了)
环境变量的组织方式
环境变量是以字符指针数组的方式进行组织的,最后的元素以NULL结尾(当程序拿到环境变量的时候,读取到NULL,(就知道读取完毕了
代码获取环境变量
main函数的参数
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[], char* env[]){
printf("argc: %d\n", argc);
for(int i = 0; i < argc; i++){
if(strcmp(argv[i], "-a") == 0){
printf("%s\n", argv[i]);
printf("hhhhh, ni zhen cong ming, lijiele\n");
}
}
return 0;
}
environ
extern char **environ;这个丛全局的外部变量在libc.so当中定义,使用的时候,需要用extren关键字
#include <stdio.h>
int main(){
extern char** environ;
for(int i = 0; environ[i] != NULL; i++){
printf("%s\n\n", environ[i]);
}
return 0;
}
getenv
#include <stdlib.h> char*getenv(const char *name) ; 参数:环境变量名称—>key 返回值:环境变量的值,没找到返回NULL
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("%s\n", getenv("PATH"));
return 0;
}
进程虚拟地址空间
1单个进程打印全局变量的地址 2创建子进程之后,父子进程都打印全局变量的地址(重点),有什么发现呢?两者的全局变量地址一样 3如果一个进程修改了全局变量的值,分别打印地址,_已经变量的值(重点),有什么发现呢?和之前的理解有什么矛盾点 地址一样,变量内容不一样,所以父子进程输出的变量绝对不是同一个变量,但地址值是一样的。说明,该地址绝对不是物理地址!在Linux地址下,这种地址叫做虚拟地址
虚拟地址
我们在用C/C++语言所看到的地址(全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理OS(操作系统)需要负责将程序当中的虚拟地址,转化为物理地址
进程虚拟地址空间
操作系统为每一个进程分出来一个4G的虚拟地址空间(32位操作系用)),程序在访问内存的时候,使用的是虚拟地址进行访问。既然是操作系统虚拟出来的地址,所以并不能直接存储数据,存储数据还是在真实的物理内存当中。 所以OS带要将虚拟地址转化为物理地址进行访问(页表)
扩展:为什么操作系统需要线每一个进程都虚拟出来一个讲程地址空凤呢﹖为啥不直接让进程访问物理内存,这样不是更加快一点
原因:因为每个进程访问一个物理地址空间,就会造成不可控。在有限的内存空间中,进程是不清楚哪一个内存被其他进程使用,那些内在是空闲的,没有办法知道。所以,这种场景下,冒昧使用,一定会导致多个进程在访问物理内存的时候,出现混乱,所以,内存由操作系统统一管理起来,但是不能采用预先直接分配内存的方式,给进程。 原因:因为0S也不清楚进程能使用多少内存,使用多久。所以,就虚拟给每一个进程分配一个4g的地址(虚拟地址)当进程真正要要保存数据,或者申请内存的时候,操作虚拟地址,让操作系统在给进程进行分配。这样就比较合理(同时也会省很多空间,毕竟谁用才分配真正的物理内存)每一个进程都无感的使用拿到的虚拟地址,背后就是OS进行了转换(即页表映射)。
映射关系
页表
物理地址为了防止内存碎片采用离散分配
分段式和段页式
扩展
参考1 详细 参考2
进程优先级
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高-那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值 nice其取值范围是-20至19,一共40个级别。 PRI(新)=PRI(old)+ nice (优先级修正数值)
用top命令更改已存在进程的nice:
top —进入top后按“r” 输入进程PID 输入nice值 PRI(新)=PRI(old+ nice (优先级修正数值)
进程控制
进程创建
fork内部完成的事情:
创建子进程,子进程拷贝父讲程的PCB。
分配新的内存块和内核数据结构( task struct给子进程 将父进程部分数据结构内容拷贝至子进程 添加丁进程到系统迸程列表当中,添加到双向链表当中 fork返回,开始调度器(操作系统开始调度)调度。
用户空间内核空间
内核空间: Linux 操作系统和驱动程序运行在内核空间。换句话说,春统调用的函数都是在内核空间运行的,因为是操作系统提供的函数 用户空间: "应用程序都是运行在用户空间的。换句话说,我们程序员自己写的代码都是运行在用户空间的。但是,当程序猿写的代码调用了系统调用函数,则会切换到内核空间进行执行。执行完毕之后,在返回到用户空间继续执行用户的代码。
写时拷贝
父进程创建出来子进程时,子进程的PCB的拷贝父进程,页表也是拷贝父进程的。在最初的时候,同一个变量的虚拟地址和物理地址的映射关系的一样的,也就是说,`操作系统并没有给子进程当中的变量在物理内存当中分配空间进行存储,子进程的变量还是原来父进程的物理地址当中的内容。 当发生改变时:才以写时拷贝的方式进行拷贝一份,此时父子进程通过各自的页表,指向不同的物理地址。当不改变时:父子进程共享同一个数据
守护进程:
父进程创建子进程,让子进程执行真正的业务(进程程序替换),父进程负责守护子进程。当子进程在执行业务的时候意外“挂掉了”,父进程负责重新启动子进程。让子进程继续提供服务。
进程终止
正常终止 其中 echo $? 可以检查上一个程序中exit()输入的参数 异常终止 出错,空指针,CTRL+c等
exit和_exit的区别
exit过程 1.执行用户自定义的清理函数 2.冲刷缓冲区(c标准库的),关闭流(标准输入输出错误)等 3.终止进程(_exit)
执行用户自定义的清理函数
回调函数,
#include<iostream>
#include <stdlib.h>
using namespace std;
void back(void){
cout<<"back!!!!!!!!!!!!!";
return;
}
int main(){
atexit(back);
cout<<"1111111";
}
刷新缓冲区的方式
exit main函数的return .fflush \n也会触发
#include<cstdio>
#include<iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
int main(){
cout<<"1111111";
fflush(stdout);
_exit(1);
}
扩展:缓冲方式
全缓冲:当缓冲区写满了的时候,才进行I0 行缓冲:行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/0库执行I/0操作 不缓冲:不带缓冲。标准I/0库不对字符进行缓冲存储
进程等待
父进程进行进程等待,等待子进程退出之后,回收子进程的退出状态信,防止子进程成为僵尸进程(进程等待是最合理的解决方案)。
函数等待wait
pid_twait(int *status) :等待任一子进程 作用:等待退出的子进程,引中含义就是,回收退出的子进程的退出状态信息。 返回值: 成功:被等待子进程PID(大于0)成功返回被等待进程pid, 失败:返回-1。 参数:出参 输出型参数,获取子进程退出状态,不关心可以设置为null, 调用wait函数的调用者,想要获取子进程的退出状态信息,但是返回值已经有含义了,所以,wait获取到的子进程的退出状态信息是通过参数进行传递的。
#include<cstdio>
#include<iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;
int main(){
pid_t pid=fork();
if(pid<0){
cout<< "create failed";
}else if(pid>0){
cout<"child";
}else{
int status;
wait(&status);
cout<<status;
}
}
pstack 想要看到一个进程正在执行什么代码,可以使用pstack [pid],就可以看到进程目前在执行什么样的代码。 特性:等待子进程退出
int status:
只用到了低两个字节。分两种情况: 子进程正常退出:如果传递status的值,在会获取到子进程的退出状态 子进程异常退出:如果传递status的值,则会获取到coredump标志位+退出信号 如I何判断子进程是正常退出还是异常退出? wait返回值: 大于0并且退出信号没有被设置(==0) :正常退出 大于0并且退出信号被设置(大于的数,有值):异常退出的场景
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
pid_t pid = fork();
if(pid < 0){
printf("create sub process failed\n");
return 0;
}else if(pid == 0){
printf("i am child: pid = %d, ppid = %d\n", getpid(), getppid());
}else{
printf("i am father: pid = %d, ppid = %d\n", getpid(), getppid());
int status;
int ret = wait(&status);
if(ret > 0 && ((status & 0x7f) == 0)){
printf("child process return code is %d\n", (status >> 8) & 0xff);
}else if(ret >0){
printf("child process recvice signal is %d, codedump flag is %d\n", (status & 0x7f), (status >> 7) & 0x1);
}
}
return 0;
}
coredump标志位
ulimit -a
ulimit -c unlimited
waitpid
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
pid_t pid = fork();
if(pid < 0){
return 0;
}else if(pid == 0){
printf("i am child, pid is %d, ppid is %d\n", getpid(), getppid());
sleep(5);
}else{
printf("i am father, pid is %d, ppid is %d\n", getpid(), getppid());
int ret = 0;
do{
ret = waitpid(pid, NULL, WNOHANG);
}while(ret == 0);
}
return 0;
}
注意:非阻塞要搭配循环使用
进程程序替换
为啥需要进程程序替换 本质上就是想让进程去执行不一样的代码(程序) 因为父进程创建出来的子进程和父进程拥有相同的代码段,所以,子进程看到的代码和父进程是一样的。当我们想要让子进程执行不同的程序时候,就需要让子进程调用进程程序替换的接口)从而让子进程执行不一样的代码
原理
罹患进程中的代码段和数据段,更新堆栈
图解
execl函数
#include <stdio.h>
#include <unistd.h>
#include<iostream>
using namespace std;
int main(){
cout<<"start"<<endl;
execl("/usr/bin/pwd","pwd",NULL);
cout<<"end";
return 0;
}
execlp
#include <stdio.h>
#include <unistd.h>
#include<iostream>
using namespace std;
int main(){
cout<<"start"<<endl;
execlp("pwd","pwd",NULL);
cout<<"end";
return 0;
}
为什么execlp,第一个参数不用带有路径呢?
execlp这个函数会去搜索PATH这个环境变量,看看要替换的可执行是否在PATH环境变量对应的路径下能不能找到。能找到:正常替换,执行替换的程序。没找到:报错返回,替换失败了。
补充
函数名当中带有"l”︰传递给可执行程序的参数是以可变参数列表的方式进行传递。第一个参数,需要可执行程序本身,如果需要传递多个参数,则用“,”进行间隔末尾以NULL结尾 函数名当中带有“p”:可以使用环境变量PATH.无需写全路径。换句话说,函数会搜索环境变量PATH,找到可执行程序。所以不用写路径
execle
#include <stdio.h>
#include <unistd.h>
#include<iostream>
using namespace std;
int main(){
cout<<"start"<<endl;
extern char**environ;
int ret= execle("/usr/bin/pwd","pwd",environ);
cout<<"end"<<ret;
return 0;
}
pwd:获取当前路径,不需要环境变量
替换自己的程序
#include <iostream>
#include <cstdlib>
using namespace std;
int main(){
cout<<getenv("PATH");
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include<iostream>
using namespace std;
int main(){
cout<<"start"<<endl;
extern char**environ;
int ret= execle("/root/test/qi","pwd",NULL,environ);
cout<<"end"<<ret;
return 0;
}
execv
#include <stdio.h>
#include <unistd.h>
#include<iostream>
using namespace std;
int main(){
cout<<"start"<<endl;
char* c[10]={NULL};
c[0]="pwd";
c[1]=NULL;
int ret= execv("/usr/bin/pwd",c);
cout<<"end"<<ret;
return 0;
}
函数名字当中带有“v”:说明命令行参数是以字符指针数组的方式进行传递的,字符指针数组需要程序员自己进行定义。
execvp
#include <stdio.h>
#include <unistd.h>
#include<iostream>
using namespace std;
int main(){
cout<<"start"<<endl;
char* c[10]={NULL};
c[0]="pwd";
c[1]=NULL;
int ret= execvp("pwd",c);
cout<<"end"<<ret;
return 0;
}
execve
#include <stdio.h>
#include <unistd.h>
int main(){
printf("i am main..., process start...\n");
char* argv[10] = {NULL};
argv[0] = "ls";
argv[1] = "-a";
argv[2] = "-l";
argv[3] = NULL;
extern char** environ;
int ret = execve("/usr/bin/ls", argv, environ);
printf("if run here, execl failed : %d\n", ret);
return 0;
}
函数联系
进程程序替换+fork+进程等待
#include <stdio.h>
#include <unistd.h>
#include<iostream>
#include <sys/wait.h>
using namespace std;
int main(){
pid_t p=fork();
if(p<0){
cout<<"make fork failed";
}
else if(p==0){
cout<<"start"<<endl;
char* c[10]={NULL};
c[0]="pwd";
c[1]=NULL;
int ret= execvp("pwd",c);
cout<<"end"<<ret<<endl;}
else {
wait(NULL);
cout<<"success"<<endl;
}
return 0;
}
|