一、冯诺依曼体系结构
二、操作系统
1、概念
2、如何理解“管理”
3、系统调用和库函数概念
三、进程
1、基本概念
2、描述进程-PCB
3、查看进程
4、通过系统调用获取进程标示符
5、通过系统调用创建进程-fork初识
四、进程状态
五、僵尸进程
六、孤儿进程
七、进程优先级
1、基本概念
2、查看系统进程
3、PRI、NI、top
八、Linux的调度与切换
1、概念引入
2、活动队列&&过期队列
3、active指针和expired指针
九、环境变量
1、概念
2、常见环境变量&&查看环境变量的方法
3、main参数&&命令行参数
4、PATH
5、和环境变量相关的命令
6、环境变量的获取
a、main函数第三个参数char* env[]
b、getenv 通过环境变量的名字直接获取环境变量的内容
c、environ
我们常见的计算机,不管是台式机还是笔记本电脑,大部分都遵守冯诺依曼体系。
那么什么是冯诺依曼体系结构呢?具体长什么样呢?
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成。
CPU: 运算器,存储器等。
输入设备:话筒,摄像头,鼠标,键盘,磁盘,网卡等。
输出设备:声卡,显卡,网卡,磁盘,显示器,打印机等。
从上面举得一些例子可以看出来,有些设备只做输入,有些设备只做输入,有些设备既做输入,又做输出。
图中的存储器指的是:内存,它具有掉电易失的特性。不考虑缓存的情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备) ,外设要输入或者输出数据,也只能写入内存或者从内存中读取。也就是说,所有的设备都只能直接和内存打交道。
距离CPU越近的存储单元,效率越高,造价贵,单体容量越小。
距离CPU越远的存储单元,效率越低,造价便宜,单体容量大。
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。
简单的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理),其他程序(例如函数库,shell程序等等)。
总结:计算机管理硬件描述起来,用struct结构体, 组织起来,用链表或其他高效的数据结构。即 先描述,再组织
像C语言这一类的高级语言,为什么会具有跨平台性,可移植性呢?
task_ struct 内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间,记账号等。
- 其他信息
组织进程
(1) ps ajx
那么具体应该如何查看一个进程呢?
//先写一个死循环,用来查看进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process!\n);
sleep(1);
}
return 0;
}
运行上面代码:
观察结果可以看出来,我们的进程被查询到了,但是好像下面还多了个进程,那么下面那个又是什么呢?
因为grep是一条指令,当我们去过滤test时,它的过滤关键字里就有test这样的关键字,所以它把自己也会过滤出来,所以就出现了下面的那个进程。
这里可以看出,几乎所有的指令就是程序,在运行起来时也要变成进程。
那么我不想看见下面多的那行应该怎么办呢?可以加上grep -v 进行反向匹配。
在上面我们使用ls /proc 这个命令时,似乎需要用到进程标识符(PID),那么PID该如何获取呢?
这个时候我们就需要通过系统调用getpid()来获取进程的pid了,同时我们观察到还有一个ppid,这个ppid是父进程的标识符(一般在Linux中,普通进程都有它的父进程),要用getppid()来获取。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = getpid();
pid _t fid = getppid();
while(1)
{
printf("I am a process, my pid is: %d, my ppid is: %d\n",id,fid);
sleep(1);
}
return 0;
}
运行我们的代码:
发现我在每一次启动进程是pid几乎都会变化,这是因为我的进程是一个新的进程。然而,我又发现,我的ppid不管哪一次启动结果都是一样的。 那么这个ppid对应的进程究竟是谁呢?
验证发现, 这个这个进程叫bash,我们在命令行启动的程序,最终转化成进程都是bash的子进程。bash是命令行解释器,所以我们在命令行启动的进程都是bash的子进程。
首先我们先运行man fork 查看一下手册,看看fork是如何使用的。
先来看这么一段代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("before fork: I am a process, pid: %d, ppid: %d\n",getpid(),getppid());
fork();
printf("after fork: I am a process, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
return 0;
}
运行后我们发现,我明明只写了两个个printf,但是这里结果为什么会打印三个结果出来呢?
原因很简单,手册里面也说了,fork有两个返回值,如果fork成功了,父进程的返回值是子进程的pid,子进程的返回值是0。
一般而言,我们想让父子做不同的事情,所以,fork 之后通常要用 if 进行分流。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("before fork: I am a process, pid: %d, ppid: %d\n",getpid(),getppid());
pid_t id = fork();
if(id<0) return 1;
else if(id==0)
{
//子进程
printf("after fork: I am a process, pid: %d, ppid: %d, return id: %d\n",getpid(),getppid(),id);
sleep(1);
}
else{
//父进程
printf("after fork: I am a process, pid: %d, ppid: %d, return id: %d\n",getpid(),getppid(),id);
sleep(1);
}
sleep(1);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("before fork: I am a process, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(5);
printf("开始创建进程啦!\n");
sleep(1);
pid_t id = fork();
if(id<0) return 1;
else if(id==0)
{
while(1){
//子进程
printf("after fork,我是子进程: I am a process, pid: %d, ppid: %d, return id: %d\n",getpid(),getppid(),id);
sleep(1);
}
}
else{
while(1){
//父进程
printf("after fork,我是父进程: I am a process, pid: %d, ppid: %d, return id: %d\n",getpid(),getppid(),id);
sleep(1);
}
}
sleep(1);
return 0;
}
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id=fork();
if(id<0) return 1;
else if(id==0)
{
int cnt=5;
//子进程
while(cnt)
{
printf("child is running, cnt: %d, pid: %d\n",cnt,getpid());
cnt--;
sleep(1);
}
printf("子进程开始退出!\n");
sleep(1);
exit(0); //让子进程直接退出
}
//只有父进程可以执行到这
sleep(10);
return 0;
}
那么僵尸进程有什么危害呢?
- 进程的退出状态必须被维持下去,因为他要告诉父进程,你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就会一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB就一直都要维护。
- 如果一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,还会造成内存泄漏。
与之相对,我们知道了僵尸进程,就也有必要了解一下孤儿进程。那什么是孤儿进程呢?
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0) return 1;
else if(id == 0)
{
//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else
{
//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
printf("父进程退出!\n");
exit(0);
}
return 0;
}
其中:
这里的PRI代表这个进程可被执行的优先级,其值越小越早被执行。
NI代表这个进程的nice值。
PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值。nice其取值范围是-20至19,一共40个级别。注意:进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
用top命令更改已存在进程的nice:
看看下面这种情况:
为什么我明明设置的nice值事25,为什么变成19了呢?其实上面已经说了,nice值是有范围的。那么为什么linux调整优先级是要受的?
如果不加,将自己的进程优先级调整的非常高,别人的优先级调整的非常低,优先级较高的进程,优先得到资源,后续还有很多进程产生,常规进程很难享受到CPU资源,这个时候就会造成进程饥饿问题。
活动队列:
- 时间片还没有结束的所有进程都按照优先级放在该队列。
- nr_active: 总共有多少个运行状态的进程。
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
- bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
过期队列:
- 过期队列和活动队列结构一模一样。
- 过期队列上放置的进程,都是时间片耗尽的进程。
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
- active指针永远指向活动队列。
- expired指针永远指向过期队列。
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。那应该怎么办呢?---> 在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
那么该如何查看呢? ---- echo $NAME //NAME你的环境变量名称
#include <stdio.h>
int main(int argc,char*argv[])
{
for(int i=0;i<argc;i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
return 0;
}
那既然是以NULL结尾,我就可以给代码改成这样:
#include <stdio.h>
int main(int argc,char*argv[])
{
for(int i=0;argv[i];i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
// for(int i=0;i<argc;i++)
// {
// printf("argv[%d]: %s\n",i,argv[i]);
// }
return 0;
}
argc: 是一个表示命令行参数的个数的整数,至少为1,因为程序名本身也算是一个参数。
agrv[]:是一个指针数组,argv[0]就是程序名称,argv[1]是第一个参数,argv[2]是第二个参数,以此类推。
那么基于这两个参数,我们来看一段代码:
#include <stdio.h>
#include <string.h>
//要实现3种不同的功能
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("Usage:\n\t%s -number[1-3]\n",argv[0]);
return 1;
}
if(strcmp("-1",argv[1])==0)
{
printf("执行功能1!\n");
}
else if(strcmp("-2",argv[1])==0)
{
printf("执行功能2!\n");
}
else if(strcmp("-3",argv[1])==0)
{
printf("执行功能3!\n");
}
else
{
printf("未知功能!\n");
}
return 0;
}
如此一来,我们就可以通过不同的选项让我们的同一个程序执行它内部的不同的功能。所以,命令行参数是Linux指令的基础。
我们发现,为什么我们在执行系统的指令,如:ls,pwd...命令时,不需要加 " ./ ",而在执行我们自己的程序时就需要加" ./ "呢?
这是因为,我们系统的指令的路径被添加到了环境变量PATH中,而我们的程序并没有被添加到环境变量PATH中,所以我需要告诉它我的程序在当前路径下,所以就要加上" ./ "。
那么我现在不想我的程序加上"./ ",我应该怎么办呢?
方法一:把我自己的程序拷贝到PATH里的任意一条路径下
方法二:把我自己当前的路径添加到PATH环境变量中
1. echo: 显示某个环境变量值2. export: 设置一个新的环境变量3. env: 显示所有环境变量4. unset: 清除环境变量5. set: 显示本地定义的shell变量和环境变量
我们刚刚讲main函数的参数,其实main函数还有一个参数char* env[]。这个char* env[]的结构和char* agrv[]一样。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char* argv[],char* env[])
{
for(int i=0;env[i];i++)
{
printf("-----------------env[%d] -> %s\n",i,env[i]);
}
return 0;
}
上面说,环境变量是可以被子进程继承下去的,如何验证呢?
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char* argv[],char* env[])
{
printf("I am a process,pid: %d,ppid: %d\n",getpid(),getppid());
for(int i=0;env[i];i++)
{
printf("-----------------env[%d] -> %s\n",i,env[i]);
}
pid_t id=fork();
if(id==0)
{
printf("---------------------------------------------");
printf("I am a process,pid: %d,ppid: %d\n",getpid(),getppid());
for(int i=0;env[i];i++)
{
printf("---------------env[%d] -> %s\n",i,env[i]);
}
}
sleep(1);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
int main()
{
const char* username=getenv("USER");
const char* home=getenv("HOME");
printf("username: %s\n",username);
printf("home: %s\n",home);
return 0;
}
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。libc(C标准库)中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
int main()
{
extern char** environ;
//environ
for(int i=0;environ[i];i++)
{
printf("environ[%d]: %s\n",i,environ[i]);
}
return 0;
}
先来观察两段代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
//定义一个全局变量
int g_val=0;
int main()
{
pid_t id=fork();
if(id<0) return 1;
else if(id==0)
{
printf("I am child,my pid is: %d ,g_val: %d ,my address is: %p\n",getpid(),g_val,&g_val);
}
else{
printf("I am father,my pid is: %d ,g_val: %d ,my address is: %p\n",getpid(),g_val,&g_val);
}
sleep(1);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
//定义一个全局变量
int g_val=0;
int main()
{
pid_t id=fork();
if(id<0) return 1;
else if(id==0)
{
g_val=100;
printf("I am child,my pid is: %d ,g_val: %d ,my address is: %p\n",getpid(),g_val,&g_val);
}
else{
printf("I am father,my pid is: %d ,g_val: %d ,my address is: %p\n",getpid(),g_val,&g_val);
}
sleep(1);
return 0;
}
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
但地址值是一样的,说明,该地址绝对不是物理地址。在Linux地址下,这种地址叫做虚拟地址。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将虚拟地址转换成物理地址。
- 将物理内存从无序变有序,让进程以统一的视角,看待内存
- 将进程管理和内存管理进行解耦合
- 地址空间+页表是保护内存安全的重要手段
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- yule263.com 版权所有 湘ICP备2023023988号-1
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务