Linux进程管理
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
文章目录
冯诺依曼体系结构
我们熟悉的计算机——笔记本不熟悉的计算机——服务器他们都是在冯诺依曼体系结构的基础上底层搭载不同的硬件结构上层由操作系统管理的。冯诺依曼体系结构如下图所示
- 输入设备包括键盘、鼠标、磁盘、网卡、摄像头、话筒等
- 存储器内存
- 中央处理器包含运算器和控制器
- 输出设备显示器、磁盘、网卡、音响等
在这里我们需要注意以下几点
- 存储器所指的是内存并非磁盘硬盘等存储器件。
- CPU只能与内存进行读写不能访问输入输出设备
- 输入输出设备读写数据也只能与内存进行交互
总结内存是体系结构的核心设备CPU与外设之间的信息交互都需要依靠内存
操作系统
什么是操作系统呢
操作系统就是一款专门针对软硬件资源进行管理工作的软件
为什么需要操作系统呢
对下与硬件交互管理所有的软硬件资源
对上为用户程序应用程序提供一个良好的执行环境
操作系统如何管理
先描述用struct结构体描述对象
再组织用链表或者其他搞笑的数据结构进行组织
系统调用与库函数
系统调用是操作系统对外提供的一些接口供上层开发使用。
库函数是存放在函数库中的函数。
那么系统调用和库函数有什么关系呢
系统调用和库函数是上下层关系库函数是用户对系统调用的进一步封装库函数对硬件进行操作时会调用系统提供的API。
进程
通俗来讲进程就是一个正在执行的程序。在这里我们理解为进程由进程控制块PCB、数据和代码构成。当然进程不止这几部分还有进程地址空间和页表等。
描述进程
操作系统中同时存在许多进程每个进程各不相同操作系统如何管理不同的进程呢
用操作系统的六字真言“先描述再组织”描述进程用到进程控制块PCB在Linux操作系统下的PCB称为task_struct。
task_struct中存储的进程的信息其主要可以分为以下几类
- 标识符也叫做PID描述本进程的唯一标识符用来区别其他进程
- 状态任务状态、退出代码、退出信号等
- 优先级相对于其他进程的优先级
- 程序计数器PC指针用于保存程序下一条执行指令的地址
- 内存指针包括程序代码和进程相关数据的指针通过内存指针可以找到程序文件
- 上下文数据进制执行时CPU的寄存器中数据
- I/O状态信息包括显示的I/O请求分配给进程的I/O设备和被进程使用的文件列表等
- 记账信息包括处理器时间总和使用的时钟数总和、时间限制、记账号等
- 其他信息
上下文数据非常重要因为在进程切换时寄存器中的数据会被保存在PCB中为了下次切换回来时CPU可以找到上次进程运行的地方。通过上下文数据我们才可以感受到进程是被切换的。
查看进程
查看进程的方法有两种
首先我们写一个死循环的程序让程序一直运行着并输出该进程的PID和PPID。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
using namespace std;
int main()
{
while(1)
{
std::cout << "pid =" << getpid() << " ppid =" << getppid() << std::endl;
sleep(1);
}
return 0;
}
- 通过/proc系统文件查看
- 通过ps - axj | grep 文件名
创建进程
我们通过系统调用fork函数创建进程
#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{
// fork的验证
int ret = fork();
std::cout << "ret =" << ret << ",proc =" << getpid() << " parent =" << getppid() << std::endl;
sleep(1);
return 0;
}
运行该程序我们可以得到以下结果
从以上结果我们可以得到以下结论
- 创建子进程后会有两个返回值父进程的返回值是子进程的PID子进程的返回值为0
- 父进程和子进程的代码共享
但通常情况下fork创建子进程后我们需要通过返回值进行分流操作目的是为了子进程与父进程做不一样的事情。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << endl;
pid_t ret = fork();
if(ret == 0)
{
while(1)
{
cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(1);
}
}
else if(ret > 0)
{
while(1)
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(2);
}
}
else
{
cout << "fork failed" << endl;
}
sleep(1);
return 0;
}
执行上述程序后的结果为
从以上结果我们可以得到以下结论
- 由于PC指针的存在fork创建的子进程并不执行fork语句前的代码
通过以上两段程序我们该如何理解fork创建进程呢
fork创建进程表示系统中多了一个进程而进程由与进程相关的内核数据结构和进程的数据和代码组成。
那么子进程的内核数据结构、数据和代码从何而来呢
子进程的task_struct会以父进程为模板初始化子进程的task_struct
子进程的代码和父进程共享一份代码因为程序运行的时间代码无法被修改
子进程的程序默认情况下子进程和父进程数据共享但是当数据发现改变的时间会“写时拷贝”
进程状态
进程控制块中有一个叫进程状态的信息进程状态标志着此进程当前的运行情况。一个进程可以有以下几种状态
- R 运行状态进程不一定在运行表明进程要么在运行中要么在运行队列里。
- S 睡眠状态可中断睡眠进程在等待事件完成此时进程处于等待队列。
- D 磁盘休眠状态不可中断睡眠状态在这个状态的进程通常会等待IO的结束。
- T 停止状态发送
SIGSTOP
信号停止进程。发送SIGCONT
信号让进程继续运行。 - X 死亡状态这个状态只是一个返回状态你不会在任务列表里看到这个状态。
Z-僵尸进程
- 僵尸状态是一个比较特殊的状态当进程退出并且父进程没用读取到子进程的退出码时就会产生僵尸状态。
- 僵尸进程会以终止状态保持在进程表中并且一直等待父进程读取退出状态代码
#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{
pid_t ret = fork();
if(ret > 0)
{
while(1)
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(2);
}
}
else if(ret == 0)
{
while(1)
{
cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(20);
exit(1);
}
}
else
{
exit(1);
}
sleep(1);
return 0
}
运行结果如图所示
僵尸进程如果一直不进行处理PCB需要一直维护占用系统的空间同时一个父进程创建了许多子进程如果不回收就会造成内存资源的泄漏。
孤儿进程
- 父进程如果提前退出子进程就会成为孤儿进程孤儿进程会被1号init进程领养。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{
pid_t ret = fork();
if(ret > 0)
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(10);
exit(1);
}
else if(ret == 0)
{
while(1)
{
cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(1);
}
}
else
{
exit(1);
}
sleep(1);
return 0;
}
运行结果如图所示
进程优先级
进程优先级表示CPU资源分为的先后顺序也就是指进程的优先权。
如何查看进程的优先级呢
- ps -l
- UID代表执行者的身份
- PID该进程的代号
- PPID该进程的父进程的代号
- PRI代表这个进程可被执行的优先级其值越小越好
- NI代表这个进程的NICE值
进程的PRI默认值都是80用户通过调整NICE值修改进程的优先级而NICE的取值范围为-20~19
NI的取值范围较小原因优先级再怎么设置也只能是一个相对的优先级不能出现绝对的优先级否则会出现“饥饿问题”
top指令修改已存在进程的NICE值。
top -> 按r -> 输入进程的PID -> 输入nice值
环境变量
环境变量一般是指操作系统中用来指定操作系统运行环境的一些参数。
环境变量通常具有某些特殊用途在系统中通常具有全局特性
常见的环境变量
- PATH指定命令的搜索路径
- HOME指定用户的主工作目录
- SHELL当前Shell通常是/bin/bash
和环境变量相关的命令
- echo显示某个环境变量值
- export设置一个新的环境变量
- env显示所有环境变量
- unset清除环境表里
- set显示本地定义的Shell变量和环境变量
查看环境变量的方法
echo $Name
如何通过代码获得环境变量呢
- 命令行第三个参数
int main(int argc, char* argv[], char *env[])
{
for(int i =0; env[i]; ++i)
{
printf("%d -> %s\n", i, env[i]);
}
return 0;
}
- 通过第三方变量environ获取
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char** environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
- 通过系统调用获取或设置环境变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
printf("%s\n", getenv("HOME"));
printf("%s\n", getenv("SHELL"));
return 0;
}
环境变量通常是具有全局属性的
我们通过设定了一个本地变量MYENV = “sherry”
#include <stdio.h>
#include <stdlib.h>
int main()
{
char * env = getenv("MYENV");
if(env)
{
printf("%s\n", env);
}
return 0;
}
首先在命令行设定一个本地变量MYENV
通过set | grep MYENV
显示出本地变量的值但运行env | grep MYENV
和./myproc
发现无法在环境变量中找到MYENV
说明此时MYENV只是一个本地变量不是环境变量通过export
命令将其设置为环境变量再运行env | grep MYENV
和./myproc
可以发现已经有输出结果。
当MYENV
变为环境变量后不仅仅可以通过命令输出还可以通过程序输出因此可以证明环境变量具有全局属性。
程序地址空间
在学习C语言的时候我们经常看见下面这张图
通过一段代码打印出不同区域的地址可以帮我们理解区域的划分
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main()
{
const char* s = "sherry";
printf("code addr:%p\n", main);
printf("string rdonly addr:%p\n", s);
printf("int addr:%p\n", &g_val);
printf("unint adde:%p\n", &g_unval);
char* heap = (char*)malloc(10);
printf("heap addr:%p\n", heap);
printf("stack addr:%p\n", &s);
int a = 1;
int b = 1;
int c = 1;
printf("stack addr:%p\n", &a);
printf("stack addr:%p\n", &b);
printf("stack addr:%p\n", &c);
}
那么这里的内存地址是不是我们经常说的物理内存呢
我们通过以下代码进行验证
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int g_val = 100;
int cnt = 5;
int ret = fork();
while(cnt)
{
if(ret > 0)
{
printf("parent[%d]: %d: %p\n",getpid(), g_val, &g_val);
}
if(ret == 0)
{
if(cnt == 3)
{
g_val = 200;
}
printf("child[%d]: %d: %p\n",getpid(), g_val, &g_val);
}
cnt--;
sleep(1);
}
return 0;
}
通过运行结果可以发现在子进程中修改了g_val的值父进程的g_val不改变因为“写时拷贝”的原因但是我们惊奇的发现父进程和子进程的g_val的地址一模一样。
结论这里的地址绝对不是物理地址而我们把他叫做虚拟地址
进程的虚拟地址和系统物理地址又有什么关联呢
进程的虚拟地址本质上是内核上的一中数据类型可以用结构体来描述。
struct mm_struct
{
int code_strat;
int code_end;
int init_data_strat;
int init_data_end;
int uninit_datae_strat;
int uninit_datae_end;
int heap_strat;
int heap_end;
int head_strat;
int head_end;
}
虽然每个进程都一个自己的mm_struct但是每个进程都认为他的mm_struct代表整个内存且所有的地址为0x0000…000~0xFFFF…FFF。
虚拟地址通过*_start和*_end的形式把自己划分成不同的区域不同的区域有着自己的地址界限。
通过页表和MMU建立虚拟地址和物理地址的映射关系。
OS为何要通过页表来实现虚拟地址和物理地址的映射关系呢
- 通过添加一层软件层完成对进程的内存操作进行管理本质是为了保护物理内存及各个进程的安全
用户的误操作可能会越界访问不属于自己的地址空间对其他地址上的内容进行修改等。使用虚拟内存就有效保护了真实物理内存空间上的内容。
- 将内存申请与内存使用在时间上划分清楚通过虚拟地址空间来屏蔽底层申请内存的过程达到进程读写内存和OS进行内存管理操作进行软件上的分离
也许进程会开辟一块很大的空间但是进程并不是立即使用因此OS可以开辟虚拟空间物理内存用于一些真正需要的地方等到进程需要使用时再为其开辟空间这样有效的提高效率。
- 站在CPU的角度进程统一看作使用4GB而每个空间区域的相对位置是比较确定的
操作系统只为了达到一个目的每一个进程都认为自己是独占系统资源的
通过以上的进程地址空间学习后以后我们再描述进程就不仅仅是PCB、数据和代码了。
进程 = 进程控制块task_struct + 进程地址空间mm_struct + 页表 + 数据和代码
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |