我们常见的计算机,不管是台式机还是笔记本电脑,大部分都遵守冯诺依曼体系。
那么什么是冯诺依曼体系结构呢?具体长什么样呢?
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成。
CPU: 运算器,存储器等。
输入设备:话筒,摄像头,鼠标,键盘,磁盘,网卡等。
输出设备:声卡,显卡,网卡,磁盘,显示器,打印机等。
从上面举得一些例子可以看出来,有些设备只做输入,有些设备只做输入,有些设备既做输入,又做输出。
图中的存储器指的是:内存,它具有掉电易失的特性。不考虑缓存的情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备) ,外设要输入或者输出数据,也只能写入内存或者从内存中读取。也就是说,所有的设备都只能直接和内存打交道。
为什么在体系结构中要存在内存?
从图中可以看出来:
**距离CPU越近的存储单元,效率越高,造价贵,单体容量越小。
距离CPU越远的存储单元,效率越低,造价便宜,单体容量大。 **
二、操作系统
1、概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。
*简单的理解,操作系统包括: **
内核(进程管理,内存管理,文件管理,驱动管理),其他程序(例如函数库,shell程序等等)。*
2、如何理解“管理”
首先要明确,在计算机上,第一个被加载的软件是操作系统。操作系统是一个进行软硬件资源管理的软件。
那么什么叫做管理呢??简单来说就是做决策+做执行。
管理者--做决策,被管理者--做执行。
总结:
**计算机管理硬件 **
描述起来,用struct结构体,****组织起来,用链表或其他高效的数据结构。
即****先描述,再组织
3、系统调用和库函数概念
**在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。 **
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
为什么要有操作系统,又为什么要有操作系统的管理?
对下管理好软硬件资源(手段),对上提供一个稳定,高效,安全的运行环境(目的)。
像C语言这一类的高级语言,为什么会具有跨平台性,可移植性呢?
三、进程
1、基本概念
进程==内核pcb对象 + 可执行程序,所有对进程的控制和操作,都只和进程的PCB有关,和进程的可执行程序没有关系,如果愿意的话,可以把PCB放到任何数据结构中,所以,进程==内核数据结构+可执行程序。
2、描述进程-PCB
**进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。 **
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
*task_struct----PCB***的一种 **
**在Linux中描述进程的结构体叫做task_struct。 **
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
*task_ struct***内容分类 **
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
下面是Linux源代码task_struct的部分截图:Linux 源码下载地址
3、查看进程
(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 进行反向匹配。
进程的信息也可以通过 /proc 系统文件夹查看。
Linux系统会将我们进程对应的相关信息以一个pid命名的目录的形式把我们的进程的属性放在该目录下。
4、通过系统调用获取进程标示符
在上面我们使用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的子进程。
5、通过系统调用创建进程-fork****初识
首先我们先运行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;
}
四、进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
五、****僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
#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;
}
七、进程优先级
1、基本概念
**cpu资源分配的先后顺序,就是指进程的优先权(priority)。 **
优先权高的进程有优先执行权利。
**配置进程优先权对多任务环境的linux很有用,可以改善系统性能。 **
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
2、查看系统进程
在linux系统中,用 ps –l 命令来查看系统进程:
其中:
这里的PRI代表这个进程可被执行的优先级,其值越小越早被执行。
NI代表这个进程的nice值。
** 3、PRI、NI、top**
PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
**NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 **
**PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice **
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
所以,调整进程优先级,在Linux下,就是调整进程nice值。
nice其取值范围是-20至19,一共40个级别。
注意:进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
用top命令更改已存在进程的nice:
- **输入top **
- 进入top后按“r”–>输入进程PID–>输入nice值
看看下面这种情况:
为什么我明明设置的nice值事25,为什么变成19了呢?其实上面已经说了,nice值是有范围限制的。那么为什么linux调整优先级是要受限制的?
如果不加限制,将自己的进程优先级调整的非常高,别人的优先级调整的非常低,优先级较高的进程,优先得到资源,后续还有很多进程产生,常规进程很难享受到CPU资源,这个时候就会造成进程饥饿问题。
八、Linux的调度与切换
1、概念引入
进程在运行时,放在CPU上,必须直接把进程代码跑完吗?------不是的!现代操作系统,都是基于时间片进行轮转执行的。
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
2、活动队列&&过期队列
活动队列:
- 时间片还没有结束的所有进程都按照优先级放在该队列。
- nr_active: 总共有多少个运行状态的进程。
- *queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下***标就是优先级! **
- bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
过期队列:
- 过期队列和活动队列结构一模一样。
- 过期队列上放置的进程,都是时间片耗尽的进程。
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
3、active指针和expired****指针
- active指针永远指向活动队列。
- expired指针永远指向过期队列。
- **可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。那应该怎么办呢?---> **在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
九、环境变量
1、概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
环境变量,不止一个,而是一堆,彼此之间没有关系,一般是操作系统内置的具有特殊用途的变量。系统的环境变量,本质就是系统自己开辟空间,给它名字和内容即可。
2、****常见环境变量&&查看环境变量的方法
- **PATH : 指定命令的搜索路径 **
- **HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录) **
- SHELL : 当前Shell,它的值通常是/bin/bash。
那么该如何查看呢? ---- echo $NAME //NAME你的环境变量名称
3、main参数&&命令行参数
#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指令的基础。
4、PATH
我们发现,为什么我们在执行系统的指令,如:ls,pwd...命令时,不需要加 " ./ ",而在执行我们自己的程序时就需要加" ./ "呢?
这是因为,我们系统的指令的路径被添加到了环境变量PATH中,而我们的程序并没有被添加到环境变量PATH中,所以我需要告诉它我的程序在当前路径下,所以就要加上" ./ "。
那么我现在不想我的程序加上"./ ",我应该怎么办呢?
方法一:把我自己的程序拷贝到PATH里的任意一条路径下
方法二:把我自己当前的路径添加到PATH环境变量中
5、和环境变量相关的命令
**1. echo: 显示某个环境变量值 **
**2. export: 设置一个新的环境变量 **
**3. env: 显示所有环境变量 **
**4. unset: 清除环境变量 **
5. set: 显示本地定义的shell变量和环境变量
6、环境变量的获取
a、main函数第三个参数char env[]*
我们刚刚讲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;
}
b、getenv 通过环境变量的名字直接获取环境变量的内容
#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;
}
c、environ
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\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必须负责将虚拟地址转换成物理地址。
这个图就说明了,同一个变量,地址相同,其实是虚拟地址相同;内容不同,其实是被映射到了不同的物理地址。
我们的地址空间,不具备对我们代码和数据的保存能力,是在物理内存中存放。将地址空间上的地址(虚拟)转化到物理内存中,给我们的进程提供一张映射表----页表。
那么为什么要有地址空间+页表呢?
- 将物理内存从无序变有序,让进程以统一的视角,看待内存
- 将进程管理和内存管理进行解耦合
- 地址空间+页表是保护内存安全的重要手段
同时地址空间+页表,这种结构也解决了一些问题:
比如malloc/new 申请内存:
1、申请的内存,你不一定会直接在里面使用
2、申请内存,本质是在进程的虚拟地址空间中申请
3、充分保证内存的使用率,不会空转,提升了new/malloc的速度
版权归原作者 爱吃糖的小杨 所有, 如有侵权,请联系我们删除。