我们常见的计算机或者大厂的后台服务器大部分都遵守冯诺依曼体系。
CPU关键组件: 寄存器、运算器、控制器和时钟。
寄存器: 寄存器是CPU内部的高速存储单元,用于临时存储和快速访问数据。寄存器通常比内存快得多,因此在执行指令时,数据可以从内存加载到寄存器中,进行高速处理。寄存器还用于存储临时结果、指令地址和其他重要信息,以支持CPU运算和控制。
运算器: 运算器是CPU的一部分,负责执行算术运算和逻辑运算。它接收来自寄存器的数据,并根据指令进行相应的计算,包括加法、减法、乘法、除法和逻辑运算(如与、或、非等)。运算器将这些计算的结果存储回寄存器或传递给其他部件使用。
控制器: 指导和协调CPU内部各个部件的操作,并根据指令序列控制计算机的执行步骤。它从内存中读取指令并解析,并发出相应的控制信号,以驱动寄存器、运算器和其他部件进行操作。控制器还负责处理中断、分支和循环,以保证程序的正确执行。
时钟: 时钟是计算机系统中的一个节拍器,用于同步和调度CPU的操作。它以固定的速度发出周期性的脉冲信号,规定了整个系统的工作节奏。时钟信号将指示到达控制器和其他部件,使它们在特定的时间执行特定的操作。时钟速率决定了CPU的工作速度,通常以赫兹(Hz)表示,如1 GHz(1赫兹等于1秒钟内的1个周期)。
小结: 寄存器提供了快速的存储空间,运算器执行运算,控制器指导执行流程,时钟提供同步和节拍信号。
注意:冯诺依曼体系中的存储器指的是内存。硬件级别的缓存空间
存储的层次结构:
注意:
- 不考虑缓存情况,CPU只能对内存进行读写,不能访问外设。而外设要输入或输出数据,也只能对内存进行写入或读取。所有设备都只能直接与内存打交道
- 输入输出设备访问速度很慢,而高速缓存的价格昂贵,不利于计算机普及,所以造就了当前计算机体系结构。
作用: 与计算机进行交互,将数据和指令从外部传输到计算机,以及将计算机的结果传输到外部。这些设备允许人们通过各种方式与计算机进行通信,包括输入数据、输出结果、控制程序等
常见的输入设备 : 键盘、鼠标、触摸屏、扫描仪、网卡等。
- 输入设备可以让用户输入文字、命令、图像或其他类型的数据,供计算机处理
常见的输出设备:显示器、打印机、音频设备、网卡等。
- 输出设备则将计算机处理得到的结果显示或输出给用户,
注意: 设备之间是相互独立的,需要把这些设备通过“线”连接。“线”又分为:IO总线(进行输入输出)、系统总线(进行数据交互)
但这些设备都是独立的,那如何实现计算机的功能呢?这就需要将这些设备通过"线"连接起来,那我们又将这些线进行分类,进行输入输出的,我们叫做IO总线,进行数据交互的我们叫做系统总线。
举例:qq
- 点击可执行程序(qq),将其加载到内存中,同时你的显示器也会显示qq的窗口
- 通过输入设备输入账号密码,然后需要网卡连接腾讯服务器进行验证
- 通过鼠标,找到你朋友的头像,点击->显示对话窗口
- 通过输入设备,输入信息,CPU将输入的数据通过内存处理,进行打包压缩,写回内存,通过网卡发送到腾讯服务器,通过服务器然后你朋友的网卡接收信息,加载到内存,经过CPU处理写回内存,然后显示到对话窗口。
冯诺依曼体系结构从硬件的角度,抽象的解释了一台计算机的运行逻辑。
概念: 操作系统是一款管理软硬件资源的软件
操作系统进行管理的原因: 为了给用户提供一个良好(稳定、高效、安全)的运行环境。手段: 帮助用户管理软硬件资源。
计算机组成部分:
根据上图可以看出,当程序员进行开发时,一定会和操作系统打交道(例如:屏幕显示)。因为操作系统管理着软硬件资源。但是操作系统的数据众多,为了保护自身数据的安全,也要给用户提供服务,所以操作系统以接口的方式给用户提供了调用入口。获取操作系统内部数据。
操作系统提供的接口,我们称为系统调用。用C实现的内部函数调用。∴所有访问操作系统的行为都只能通过系统调用。
操作系统的加载:
操作系统作为一款软件,也需要加载到内存。按下开机键时,BIOS(基本输入输出系统)芯片就开始启动,开始做准备工作,通电唤醒CPU,IO接口,内存等资源,有了CPU这一名大将,便可以接着加载硬件资源,比如:网卡,显卡,磁盘等。接着再找到操作系统的引导文件,借助引导文件,将操作系统从磁盘加载到内存中。
BIOS:计算机的一个在CPU运行的可执行程序,一个引子,加载基本资源,然后把操作系统带到内存当中,就可以进行管理软硬件资源。
操作系统对软硬件资源的管理:
概念: 担当分配系统资源的(CPU时间,内存)实体。(哈哈哈哈难以理解,但是后面学到线程还会介绍)。当然有些书上说,正在执行的程序。
当下的理解:进程 = 内核PCB数据结构对象(下面讲) + 代码和数据
在讲操作系统对软硬件资源管理时,强调了对资源如何管理,就是先描述再组织。 操作系统在管理这个进程时不会直接对这个程序进行管理(就比如一个程序体量很大,想要对其进行管理就得先加载到内存,但是有的程序太大,都不能加载到内存),只需要管理这个进程PCB即可。
在进程资源的管理中就是:
task_struct
,它会被装载到RAM(内存)里。Linux内核中task_struct源码:
struct task_struct {
volatile long state; //说明了该进程是否可以执行,还是可中断等信息
unsigned long flags; //Flage 是进程号,在调用fork()时给出
intsigpending; //进程上是否有待处理的信号
mm_segment_taddr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同
//0-0xBFFFFFFF foruser-thead
//0-0xFFFFFFFF forkernel-thread
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatilelong need_resched;
int lock_depth; //锁深度
longnice; //进程的基本时间片
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR,分时进程:SCHED_OTHER
unsigned long policy;
struct mm_struct *mm; //进程内存管理信息
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned longsleep_time; //进程的睡眠时间
//用于将系统中所有的进程连成一个双向循环链表,其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_headlocal_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
intpdeath_signal; //父进程终止是向子进程发送的信号
unsigned longpersonality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
intdid_exec:1;
pid_tpid; //进程标识符,用来代表一个进程
pid_tpgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_tsession; //进程的会话标识
pid_t tgid;
intleader; //表示进程是否为会话主管
struct task_struct*p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct*pidhash_next; //用于将进程链入HASH表
struct task_struct**pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct completion*vfork_done; //供vfork()使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies,系统根据it_real_value
//设置定时器的第一个终止时间.在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
//it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_listreal_timer; //指向实时定时器的指针
struct tmstimes; //记录进程消耗的时间
unsigned longstart_time; //进程创建的时间
//记录进程在每个CPU上所消耗的用户态时间和核心态时间
longper_cpu_utime[NR_CPUS],per_cpu_stime[NR_CPUS];
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copyon Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数);nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt,cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsignedlong min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_tcap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned shortused_math; //是否使用FPU
charcomm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count, total_link_count;
//NULL if no tty进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct*tty;
unsigned int locks;
//进程间通信信息
struct sem_undo*semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
structthread_struct thread;
//文件系统信息
struct fs_struct *fs;
//打开文件信息
struct files_struct *files;
//信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpendingpending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};
//用于将系统中所有的进程连成一个双向循环链表,其根是init_task
struct task_struct *next_task, *prev_task;
注:如果再有新的进程增加、删除或修改,直接对该链表进行增删查改
有了task_struct也就能找到该进程的代码和数据,就可以对该进程做管理
因此,进程 = 内核PCB数据结构对象 + 代码和数据
查看进程基本信息的手段: ps命令
进程的基本信息:
查看进程pid和ppid
一个进程只有一个pid,获得这个进程的pid需要通过
getpid()
系统调用。在后面还会涉及父进程也就是ppid,获得ppid需要通过getppid()
系统调用
演示:
注:
kill -9 进程pid
可以杀掉进程。涉及信号
通过进程pid查看进程相关信息: 操作系统如何找到可执行程序,因为有路径信息的存在。
认识fork函数 fork之后通常会用if分流,为了区分让不同的执行流执行不同的代码块。
fork是用来创建进程的,上文也提到 进程 = 内核PCB数据结构对象 + 代码和数据,那被创建出来的子进程必须有一份自己的PCB和自己的代码和数据。注:进程是具有独立性的
- 因为新创建进程了,所以操作系统需要给该进程创建相对应的task_struct,从而OS可以管理task_struct数据结构的方式来管理这个子进程。注:子进程的task_struct是根据父进程的task_struct来初始化。
- 但是代码和数据也是从父进程拷贝一份吗???
答:子进程的代码和数据,默认会和父进程使用同一套代码和数据
补充:
- 以目前所学的知识代码是不能修改的,父子进程共享同一个可执行文件,父子进程同时执行相同的代码逻辑
- 父子进程可以修改它们自己的数据。这意味着如果修改数据就不能用同一份数据
结论: 在Linux内核中,并没有给子进程单独拷贝一份数据(因为存在资源浪费,操作系统不会做浪费资源的事情(如果做了,那就属于操作系统的BUG)),而是默认数据和代码都用同一份,但是当数据被修改时,采用写时拷贝的技术。再对数据进行拷贝
- 维护了进程的独立性
- 提高了对内存资源的利用率。
写时拷贝:
注:这里并没有深入理解,如果深入理解,还要牵出线性内存和物理内存,页表等。
思考:fork如果成功有两个返回值,一个变量两个返回值,值得深思。
回答:fork是一个存在返回值的函数,而通常理解返回时已经把这个函数执行完毕,所以fork在return之前,新进程已经被建立,所以返回时是父子进程分别返回,所以就有两个返回值了
简单使用:
写时拷贝:
概念:当进程退出并且父进程没有读取到子进程退出的返回码
危害:造成内存泄漏。原因:进程的相关资源不能被释放,尤其task_struck结构体,因为虽然是僵尸状态(Z),但是要用数据维护,这也属于进程的基本信息,也保存在task_struct中
演示僵尸进程:
解决办法:
概念:父进程先于子进程退出,此时子进程称为“孤儿进程”。孤儿进程会被1号进程(OS)领养,所以有1号进程回收
注:只有父子关系进程,没有爷孙关系的进程,所以释放资源的操作要么是父进程,要么是OS亲自管理。
概念:在运行队列中的所有进程都说该进程状态是运行态。在一个时间段,所有运行态的进程都会执行,并发执行。
注:
并发: 一个CPU跑多个程序
并行: 多个CPU跑多个程序
一个CPU只有一个运行队列
寄存器:
CPU执行的是程序代码转换的汇编指令,它们直接与计算机体系结构中的硬件交互。汇编语言是机器语言的助记符表示形式,它与机器语言一一对应。想要执行汇编指令,就需要大量的寄存器。(在前面的博客也提到过)
维护当前正在使用的函数空间
- ebp:栈底寄存器
- esp:栈顶寄存器
eax:通用寄存器,保留临时数据,常用于返回值,也用于赋值。
ebx:通用寄存器,保留临时数据——通常是用于初始化用户栈
eip: 指令寄存器,保存当前指令的下一条指令的地址,确保进程继续执行下去。
- eip(也称PC指针):程序计数器。指令是按照顺序从内存中读取并执行的。PC指针就是用来记录下一条将要执行的指令的地址
概念:进程在执行过程中遇到了某种阻塞操作(如等待I/O输入输出、等待资源等),无法继续执行,进入阻塞态等待条件满足。操作系统会将进程从运行态切换到阻塞态,并将CPU资源分配给其他可执行进程。
eg:
进程或多或少都会访问外设,例如让显示器显示一个信息,但是也知道正常情况一台计算机只有一个显示器,但是在操作系统中进程很多,所以供需关系就出现问题。所以这时候进程就需要进行等待
概念:指进程被暂时中止执行之后的一种状态。当一个进程处于挂起态时,它暂时停止执行,不占用CPU资源,其运行状态和上下文信息会被保存。
- 在等待队列中,这时内存资源不足,在保证正常的情况下,将一些进程(处于空闲,没有被调度)的数据和代码交换到外设(swap分区),PCB继续排队,当进程再次进行运行时再换入进来。
- 进程主动发起挂起请求,等待某一事件发生。
挂起态的使用可以有效控制系统资源的分配和调度,允许系统暂停处于低优先级或空闲状态的进程,优化系统的性能和效率。
进程在运行中或运行队列中都是R状态
注: 前后台进程的区别:(参考)
S状态意味着进程在等待事件完成。(也可称为可中断睡眠状态)
注: 该状态,Ctrl+c和kill -9 还能杀死。可唤醒,相应请求
D状态也称不可中断睡眠状态,在进程在等待磁盘写入完毕期间,这个进程不能被任何人杀掉,不响应任何请求。kill命令也不行(区别于S状态)
解释: 当进行大量的磁盘写入,你的进程属于睡眠状态,但是如果操作系统资源不够,为了不影响运行态的进程等,直接把该睡眠态的进程杀掉,但是此时文件也没有完全写入磁盘,就会出现错误。所以为了不被打扰,所以出现D状态
可以通过发送信号进入T状态,也可以通过信号继续执行。作用:可能是等待资源,也可能是控制进程。
注: 查看信号(后面信号部分具体介绍)
信号的18和19为暂停进程和继续运行进程的信号
测试所用代码:
测试:
追踪状态,由我们控制程序一步一步执行,比如调试的过程
除此还有x状态(死亡状态)难以演示,还有僵尸状态(上面已经演示)。操作系统的原则和具体操作系统的原则是一致的,但是实现上是不同的。
提出:
资源是有限的,进程是可以很多的,注定了进程之间存在竞争关系。而操作系统必须保证所有进程是良性竞争,确认优先级。
概念:
- CPU分配资源的先后顺序,就是指进程的优先权。
- 优先权高的进程有优先执行的权利。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能
- 在多CPU中,可以把进程运行到指定的CPU中,把不重要的进程安排到某个CPU,可以改善系统整体性能
ps -len / ps -l / ps - al / top
命令都可以查看进程的优先级PRI(PR)
注: 信息
- PRI:进程优先级。即程序被CPU执行的先后顺序。值越小进程的优先级别越高
- NI:进程nice值。进程可被执行的优先级修正数。
- nice取值范围[-20, 19],一共40个级别
- PRI越小被执行的越快,加nice值之后,PRI(new) = PRI(old) + nice (PRI在Linux中默认80,NI默认为0。只要进行调整PRI就是从80计算,也就是不用看之前修改的值,因此很好的限制了PRI)
- 所以nice为负,程序的优先级值变小,即优先级变高,则越快被执行
- pri修改优先级需要比较高的权限,普通用户只能nice往大了调,使优先级降低,root用户可以往大了也可以往小了进行调,不过范围还是再[-20,19]。
调度器为了公平分配CPU资源,禁止高频次随意修改PRI,不然可能导致进程饥饿问题(也就是该进程分配不到CPU资源)。Linux对用户能修改的优先级进行了限制,也就是nice的范围,避免一个进程的优先级很高或很低(几乎一直被调度或者几乎不被调度)。小结:调整进程优先级,在Linux中就是调整nice值
top和renice命令调整PRI
top命令更改已存在进程的nice的流程
使用top命令->按r键(进入top后)->输入进程PID->输入nice的值。
注:
- 如果输入的nice的值超出范围,会按照最高值或最低值计算
- 把PRI的优先级调高,需要root权限。
流程图:
注: 想把NI设置成负数,需要root权限
语法:
renice -n [nice值] [进程pid]
- 竞争性:系统进程数目众多,而CPU资源只有少量甚至就一个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理分配资源,便具有了优先级
- 独立性:多进程运行,需要独享各种资源,各个进程运行期间互不干扰
- 并行:多个进程在多个CPU下分别同时运行
- 并发:多个进程在一个CPU下采用进程切换(切换上下文)的方式,在一段时间内,让多个进程得以推进
一个进程的时间片到了,进程需要从CPU切下去,要将自己的上下文数据保存好,带走。目的就是为了未来恢复。上下文数据:CPU寄存器里面保存的是进程的临时数据,会被更改。
进程被切换时:
- 保存上下文——> 保存在进程PCB中
2.恢复上下文
CPU中有很多寄存器
- 通用寄存器:eax, ebx, ecx, edx
- 栈帧:ebp, esp, eip
- 状态寄存器:status
其中eip也是我们常说的pc(程序计数器):用于记录当前进程正在执行指令的下一行指令的地址。
概念:指在操作系统中用来指定操作系统运行环境的一些参数。
注:
- 是系统提供的一组name = value形式的变量,环境变量通常具有某些特殊用途,不同的环境变量用途也不同,在系统当中通常具有全局特性。
- 环境变量是内存级的,关掉XShell就没了,启动时环境变量保存在系统的配置文件中就直接加载了。
查看环境变量的方法:
echo $name
//name:需要查看的环境变量名称
指定命令的搜索路径
在初识Linux基本指令部分就提到。指令就是可执行程序,和我们自己写的代码编译好的程序没有区别。
问题: 为什么Linux的指令执行和我们自己编写好的程序,在执行上有区别?
解释: 因为指令都放在user/bin
的路径下,bash命令行在查找时根据环境变量会到该目录下查找指定的指令。而我们写的程序,没有在该环境变量指定的目录下,所以需要一个完整的目录结构,所以要携带./ (当前路径)
解决办法: 不想添加./
user/bin
路径下方法1就不演示,直接mv过去即可
方法2演示: 增加搜索路径
注:
- PATH=$PATH:路径(不影响使用正常指令)
- PATH=路径(覆盖了前面的搜索路径,所以指令也就不能运行了)
- 再赋值的时候不能有空格
因为环境变量是内存级的,所以不用担心路径填写出错
指定用户的主工作目录(即用户登陆到Linux系统时默认的目录)
可以更改,但是更改之后,可能cd ~
该指令就不能使用
查看当前使用的shell命令
语法:
export [变量名=值]
头文件:
#include <stdlib.h>
函数声明:char *getenv(const char *name);
使用:
所以:可以根据获取同一个环境变量的值不同,做出对应的操作。
在一些教材中,我们可以看到main函数有这样两个参数
int main(int argc, char* argv[])
{}
结论:
- 命令行参数的作用:为指令,工具,软件等提供命令行选项支持。根据不同的选项从而实现不同的功能。
- argc就是argv中字符串的个数。其中对传入的参数用空格分隔。argv(命令行参数的向量表)就是分割后存储字符串的指针数组
命令行参数底层存储: 这就是命令行参数表
根据这个表最后一个是NULL,所以我们在获取参数时,也可以这样:
int main(int argc, char* argv[])
{
int i = 0;
for(; argv[i]; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
在main函数中还有第三个参数,就是环境变量参数
int main(int argc, char* argv[], char* env[]) //最后一个参数就是环境变量参数
{}
结论:
- 环境变量是能够被从父进程中继承下来,并放在environ中
- 本地变量不能被继承
注:我们在使用指令时,大部分都是bash创建子进程执行指令。还有一部分是内建命令,不需要创建子进程。—— 所以我们在查看本地变量是依旧能直接查看,因为本地变量就是在bash中,而echo就属于一个命令。
和环境变量相关的命令:
echo $NAME
//NAME:环境变量名称。作用:显示环境变量的值- export 作用:设置一个新的环境变量
- env 作用:显示所有环境变量
- unset [变量名] 作用:清除环境变量
- set 作用:显示本地定义的shell变量和环境变量
两种核心向量表:
- 命令行参数表
- 环境变量表
两批命令:
- 常规命令:通过创建子进程完成
- 内建命令:bash不用创建子进程,而是由自身直接完成。(bash调用自己写的函数,或者是系统提供的函数) 例如:echo,cd(cd就是调用系统调用chdir)