进程基本概念
课本概念:程序的一个执行实例,正在执行的程序等 内核观点:担当分配系统资源(CPU时间,内存)的实体。
描述进程-PCB
系统中可不可能存在大量的进程 ?可能, 操作系统管理进程 ? 必须的。 如何管理进程呢?? 先描述,在组织 任何进程在形成之时,操作系统要为该进程创建PCB,进程控制块
struct PCB{
}
在OS上面,PCB,进程控制块,就是一个结构体类型 在Linux系统中,PCB->struct task_struct {//进程的所有的属性}
程序文件内容+相关的数据结构=进程
上面5个框框相当于磁盘中的可执行程序加载到内存中,形成5个进程,操作系统为了管理这5个进程必须先描述,在组织;为每一个进程创建一个task_struct的结构体,描述的是每一个进程的相关属性 查找进程就是找进程的task_struct
下图就是一个完整的进程 有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系!!!,与进程对应的内核创建的该进程的PCB强相关! 进程 vs 程序 曾经我们所有的启动程序的过程,本质都是在系统上面创建进程!! !
task_struct-PCB的一种
在Linux中描述进程的结构体叫做task_struct。 task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。 状态: 任务状态,退出代码,退出信号等。 优先级: 相对于其他进程的优先级。(先后的问题) 程序计数器: 程序中即将被执行的下一条指令的地址。 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针 上下文数据: 进程执行时处理器的寄存器中的数据 [休学例子,要加图CPU,寄存器]。 I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。 其他信息
通过上下文,我们能感受到进程是被切换的!!! 为了让你去做其他事情,但不耽误当前,并且,当你想回来继续学习的时候,可以接着之前你的学习内容,继续学习
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
查看进程
通过系统调用获取进程标示符
进程id(PID) 父进程id(PPID)
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(){
while(1){
printf("hello world! pid: %d\n",getpid());
sleep(1);
}
return 0;
}
这是死循环的代码,方便我们查看 <sys/ypyes.h> 是系统头文件
上图展示的就是linux环境下创建进程和结束进程 通过kill -9 进程pid 可结束进程 在命令行上运行的命令,基本上父进程都是bash
查看进程的第二种方式
通过系统调用创建进程-fork初识
运行 man fork 认识fork fork有两个返回值 父子进程代码共享(代码只有一份,不可以被修改),数据各自开辟空间,私有一份(通过“写时拷贝"来完成进程数据的独立性) fork 之后通常要用 if 进行分流
如何理解fork创建子进程
- ./cmd or run command,fork:在操作系统角度,上面的创建进程的方式,没有差别的! ! !
- fork本质是创建进程,也就是系统里多了一个进程 与进程相关的内核数据结构(task_struct)+进程的代码和数据在系统里面多了一份
我们只是fork了,创建了子进程,但是子进程对应的代码和数据呢? ? ? 默认情况下,会“继承”父进程的代码和数据 内核数据结构task_struct ,也会以父进程为模板,初始化子进程的task struct
我们创建的子进程,就是为了和父进程做一样的事情??? 这样一般是没有意义的 ! ! ! 通过if else 分流,让父子做不一样的事情! fork的返回值来完成!!
失败: <0 成功 ①给父进程返回子进程的pid ②给子进程返回0
#include<iostream>
#include<unistd.h>
int main(){
pid_t id=fork();
if(id ==0 ){
while(true){
std::cout<<" I am child, pid: "<<getpid()<<",ppid:"<<getppid()<<std::endl;
sleep(1);
}
}else if(id > 0){
while(true){
std::cout<<" I am father, pid: "<<getpid()<<",ppid: "<<getppid()<<std::endl;
sleep(2);
}
}else{
}
sleep(1);
return 0;
}
fork之后,父子谁先运行?? 这是不确定的,需要看调度器
进程状态
看看Linux内核源代码怎么说
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。 下面的状态在kernel源代码里定义:
static const char * const task_state_array[] = {
"R (running)",
"S (sleeping)",
"D (disk sleep)",
"T (stopped)",
"t (tracing stop)",
"X (dead)",
"Z (zombie)",
};
··R运行状态(running) :并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。 ··S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠。 ··D磁盘休眠状态(Disksleep):有时候也叫不可中断睡眠状态(深度睡眠)(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。 ··T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT信号让进程继续运行。 ··X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程的状态信息在哪里呢?? --> task_struct (PCB) 进程状态的意义:方便OS快速判断进程,完成特定的功能,比如调度,本质是一种分类!
R运行状态(running)
当具体状态为R时,也就是运行状态,一定正在占有CPU吗?? 不一定!!! 千万不要认为,进程只会等待CPU资源!!!还有其他资源!!
演示
S睡眠状态(sleeping) D磁盘休眠状态(Disksleep)
当我们完成某种任务的时候,任务条件不具备,需要进程进行某种等待(S,D)。
我们把,从运行状态的task_struct(run_queue),放到等待队列中,就叫做挂起等待(阻塞)从等待队列,放到运行队列,被CPU调度就叫做唤醒进程!!
所谓的进程在运行的时候,有可能因为运行需要,可以会在不同的队列里!!! 在不同的队列里,所处的状态是不一样的!!
演示
X死亡状态(dead)
死亡 --> 回收进程资源 = 进程相关的内核数据结构 + 你的代码和数据
Z(zombie)-僵尸进程
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
为什么要有僵尸状态呢??? --> 为了辨别死亡原因 !!进程退出的信息(数据)
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢? 父进程先退出,子进程就称之为“孤儿进程” 孤儿进程被1号init进程领养,当然要有init进程回收喽。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}else if(id == 0){
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
进程优先级
基本概念
为什么会有优先级? 因为资源太少! 本质是一种分配资源的方式
cpu资源分配的先后顺序,就是指进程的优先权(priority)。 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份 PID : 代表这个进程的代号 PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号 PRI :代表这个进程可被执行的优先级,其值越小越早被执行 NI :代表这个进程的nice
PRI and NI
·· PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级别越高 ··那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 ·· PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice ··这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行 ··所以,调整进程优先级,在Linux下,就是调整进程nice值
PRI vs NI
·· 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。 ·· 可以理解nice值是进程优先级的修正修正数据,而且只能是一个相对比较小的范围,因为优先级再怎么设置,也只能是一种相对优先级,不能出现绝对优先级,否者会出现严重的"饥饿问题",调度器的作用就是较为均衡的让每个进程享受到CPU资源
查看进程优先级的命令
用top命令更改已存在进程的nice:
top 进入top后按“r”–>输入进程PID–>输入nice值
其他概念
竞争性 : 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级 独立性 : 多进程运行,需要独享各种资源,多进程运行期间互不干扰 并行 : 多个进程在多个CPU下分别,同时进行运行,这称之为并行 并发 : 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
环境变量
在Linux下执行程序需要路径./name 为何系统命令不用带路径呢???因为环境变量
基本概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
语言上面定义变量本质是在内存中开辟空间 (有名字) 不要去质疑OS开辟空间的能力 !! 环境变量本质是OS在内存/磁盘文件中开辟的空间,用来保存系统相关的数据 !!
常见环境变量
PATH : 指定命令的搜索路径 HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录) SHELL :当前Shell,它的值通常是/bin/bash。
查看环境变量方法
echo $NAME //NAME:你的环境变量名称
和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
通过代码如何获取环境变量
命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", 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;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
通过系统调用获取或设置环境变量
int main(){
printf("PATH: %s\n",getenv("PATH"));
printf("HOME: %s\n",getenv("HOME"));
printf("SHEEL: %s\n",getenv("SHEEL"));
}
环境变量通常具有全局属性,可以被子进程继承下去
程序地址空间
研究背景 kernel 2.6.32 (32位平台)
程序地址空间回顾
我们在讲C语言的时候,画过这样的空间布局图
来段代码感受一下
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int g_unval;
int g_val =100;
int main(){
const char *s ="hello world";
printf("code addr: %p\n", main);
printf("string rdonly addr: %p\n", s);
printf("uninit addr: %p\n", &g_unval);
printf("init addr:%pln" , &g_val);
char *heap = (char*)ma1loc(10);
printf("heap addr: %p\n", heap);
printf("stack addr: %p\n", &s);
printf("stack addr:%pln", &heap);
int a = 10;
int b = 30;
printf("stack addr: %p\n", &a);
printf("stack addr: %p\n", &b);
return 0;
}
结果显而易见 再看下面这段代码
int g_val = 100;
int main(){
if(fork() == o){
int cnt = 5;
while(cnt){
printf("I am child, times: %d, g_val = %d,&g val = %p\n ", cnt,g val,&g_val);
cnt--;
sleep(1);
}
}else{
while(1){
printf("I am child,g_val = %d,&g_val = %p\n", g_val,&g_val);sleep(1);
}
}
return 0;
}
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动:
int g_val = 100;
int main(){
if(fork() == o){
int cnt = 5;
while(cnt){
printf("I am child, times: %d, g_val = %d,&g val = %p\n ", cnt,g val,&g_val);
cnt--;
sleep(1);
if(cnt == 3){
printf("##################child数据更改##################\n");
g_val = 200;
printf("##################child数据更改完成#######################\n");
}
}
}else{
while(1){
printf("I am father,g_val = %d,&g_val = %p\n", g_val,&g_val);sleep(1);
}
}
return 0;
}
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
··变量内容不一样,所以父子进程输出的变量绝对不是同一个变量 ··但地址值是一样的,说明,该地址绝对不是物理地址! ··在Linux地址下,这种地址叫做虚拟地址 ··我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化 成物理地址 。
进程地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
每个私生子都被画了一张大饼,都认为自己有100亿 每个进程都有一个进程空间,都认为自己在独占物理内存!!
上面的图就足矣说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
|