💁♂️个人主页:进击的荆棘
👇作者其它专栏:
目录
1.冯诺依曼体系结构
2.操作系统(Operator System)
3.进程
4.命令行参数和环境变量
5.程序地址空间
1.冯诺依曼体系结构
常见的计算机,如笔记本。不常见的计算器,如服务器,大部分都遵守冯诺依曼体系。

截止目前,我们所认识的计算机,都是由一个个的硬件组件组成
●输入单元:键盘,鼠标,扫描仪,写板等
●中央处理器(CPU):运算器和控制器等
●输出单元:显示器,打印机等
关于冯诺依曼:
●这里的存储器指的是内存
●不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)(数据层面)
●外设(输入或输设备)要输入或输出数据,也只能写入内存或从内存中读取
●即所有设备都只能直接和内存打交道

体系结构的效率由"拷贝"效率决定
注意:
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上(如:qq上和朋友聊天,数据的流动过程)
2.操作系统(Operator System)
2.1概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
●内核(进程管理,内存管理,文件管理,驱动管理)
●其它程序(例如函数库,shell程序等等)

2.2设计OS的目的
●对上,与硬件交互,管理所有的软硬件资源
●对下,为用户程序(应用程序)提供一个良好的执行环境

2.3核心功能
●在整个计算机软硬件架构中,操作系统的定位是:一款纯正的"搞管理"的软件
2.4如何理解"管理"
●管理的例子-学生,辅导员,校长
●描述被管理的对象
●组织被管理对象




总结
计算机管理硬件
1.描述起来,用struct结构体
2.组织起来,用链表或其它高效的数据结构
2.5系统调用和库函数概念

●在开发角度,操作系统对外会表现为一个整体,到那时会暴露自己的部分接口,供上层开发使用,这部份由操作系统提供的接口,叫做系统调用
●系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利与更上层用户或开发者进行二次开发
操作系统是怎样进行进程管理的?
先把进程描述起来,再把进程组织起来
3.进程
3.1基本概念与基本操作
●课本概念:程序的一个执行实例,正在执行的程序等
●内核观点:担当分配系统资源(CPU时间,内存)的实体
●当前:进程=内核数据结构(task_struct)+自己的程序代码和数据
3.1.1描述进程-PCB
基本概念
●进程信息被放到一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
●课本上称之为PCB(process control block),Linux操作系统下的PCB是task_struct
task_struct PCB的一种
●在Linux中描述进程的结构体叫做task_struct
●task_struct是Linux内核的一种数据结构类型,它会被装载到RAM(内存)里并且包含着进程的信息
3.1.2task_struct
内容分类
●标识符:描述本进程的唯一标识符,用来区别其它进程
●状态:任务状态,退出代码,退出信号等
●优先级:相对于其它进程的优先级
●程序计数器:程序中即将被执行的下一条指令的地址
●内存指针:包括程序代码和进程相关数据的指针,还有和其它进程共享的内存块的指针
●上下文数据:进程执行时处理器的寄存器中的数据休学例子,要加图CPU,寄存器
●I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
●记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记帐号等
●其它信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct双链表的形式存在于内核里

3.1.3查看进程
1.进程的信息可以通过/proc系统文件夹查看
如:要获取PID为1的进程信息,需要查看/proc/1这个文件夹

2.大多数进程信息同样可以使用top和ps这些用户级工具来获取
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
while(1){
sleep(1);
}
return 0;
}

3.1.4通过系统调用获取进程标识符
●进程id(PID)
●父进程id(PPID)
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
printf("pid:%d\n",getpid());
printf("ppid:%d\n",getppid());
return 0;
}
3.1.5通过系统调用创建进程-fork
●运行man fork认识fork
●fork有两个返回值
●父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
int ret=fork();
printf("hello proc:%d,ret:%d\n",getpid(),ret);
sleep(1);
return 0;
}
●fork之后通常要用if进行分流
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
int ret=fork();
if(ret<0){
perror("fork");
return 1;
}
else if(ret==0){
printf("I am child:%d,ret:%d\n",getpid(),ret);
}
else{
printf("I am father:%d,ret:%d\n",getpid(),ret);
sleep(1);
return 0;
}
3.2进程状态

3.2.1Linux内核源代码
●为了理解正在运行的进程是什么意思,需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)
下面的状态在kernel源代码里定义:
cpp
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
static const char *const task_state_array[] = {
"R (running)", /*0 */
"S (sleeping)", /*1 */
"D (disk sleep)", /*2 */
"T (stopped)", /*4 */
"t (tracing stop)", /*8 */
"X (dead)", /*16 */
"Z (zombie)", /*32 */
};
●R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
●S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
●D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束
●T停止状态(stopped):可以通过发送SIGSTOP信息给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行
●X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表中看到这个状态。
3.2.2进程状态查看
ps aux / ps axj 命令
●a:显示一个终端所有的进程,包括其他用户的进程
●x:显示没有控制终端的进程,例如后台运行的守护进程
●j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
●u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、cpu和内存使用情况等

3.2.3 Z(zombie)-僵尸进程
●僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程
●僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
●所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
创建一个僵尸进程:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id > 0){ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}else{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
编译并在另一个终端下监控

开始监控

结果

3.2.4僵尸进程的危害
●进程的退出状态必须被维持下去,因为它要告诉它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程若一直不读取,那子进程就一直处于Z状态
●维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
●那一个父进程创建了很多子进程,就是不回收,会导致内存资源的浪费。因为数据结构对象本身就要占用内存,像C语言中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间
●导致内存泄露
3.2.5孤儿进程
●父进程若提前退出,那么子进程后退出,进入Z之后,该如何处理?
●父进程先退出,子进程就称为"孤儿进程"
●孤儿进程被1号init/systemd进程领养,由init/systemd进程回收
cpp
#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){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}

3.3进程优先级
3.3.1基本概念
●CPU资源分配的先后顺序,就是进程的优先权(priority)
●优先权高的进程有优先执行权力。配置进程优先权对多任务环境的linux很有用,可以改善系统性能
●还能把进程运行到指定的CPU上,把不重要的进程安排到某个CPU,可以大大改善系统性能
3.3.2查看系统进程
在linux或unix系统中,用ps-l命令则会输出以下内容:

其中:
●UID:执行者的身份
●PID:进程的代号
●PPID:这个进程是由哪个进程发展衍生而来的,即父进程的代号
●PRI:这个进程可被执行的优先级,其值越小越早被执行
●NI:这个进程的nice值
3.3.3PRI and NI
●PRI即进程的优先级,或者说程序被CPU执行的先后顺序,此值越小进程的优先级越高
●NI即nice值,表示进程可被执行的优先级的修正数值
●PRI越小越快被执行,加入nice值后,将会使PRI变为:PRI(new)=PRI(默认80)+nice
●当nice值为负值时,该程序优先级值将会变小,优先级变高,越快被执行
●所以,调整进程优先级,在Linux下,就是调整进程nice值
●nice其取值范围是-20,19,共40个级别
3.3.4PRI vs NI
●进程的nice值不是进程的优先级,它们不是一个概念,但进程nice值会影响到进程的优先级变化
●nice值是进程优先级的修正数据
3.3.5查看进程优先级的命令
用top命令更改已存在进程的nice:
●top
●进入后按"r" ->输入进程PID->输入nice值
注意:
●其它调整优先级的命令:nice(未运行),renice(正在运行)
●系统函数:
cpp
#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
3.3.6竞争、独立、并行、并发
●竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
●独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
●并行:多个进程在多个CPU下分别同时进行运行
●并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进

3.4进程切换
CPU上下文切换:其实际含义是任务切换,或CPU寄存器切换。当多任务内核决定运行另外的任务时,它保存正在运行任务的当前状态,也就是CPU寄存器中的全部任务。这些内容被保存在任务自己的堆栈中,入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器,并开始下一个任务的运行,这个过程就是context switch

参考一下Linux内核0.11代码

注意:
时间片:当地计算机都是分时操作系统,每个进程都有它合适的时间片(其实就是一个计数器)。时间片到达,进程就被操作系统从CPU中剥离下来
3.5Linux2.6内核进程O(1)调度队列

上图是Linux2.6内核中进程队列的数据结构
3.5.1一个CPU拥有一个runqueue
若有多个CPU就要考虑进程个数的负载均衡问题
3.5.2优先级
●普通优先级:100~139(我们都是普通的优先级,与nice值的取值范围对应)
●实时优先级:0~99(不关心)
3.5.3活动队列
●时间片还没有结束的所有进程都按照优先级放在该队列
●nr_active:共有多少个运行状态的进程
●queue140:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以数组下标就是优先级
●从该结构中,选择一个最合适的进程,过程:
1.从0下标开始遍历queue140
2.找到第一个非空队列,该队列必定为优先级最高的队列
3.拿到选中队列的第一个进程,开始运行,调度完成
4.遍历queue140时间复杂度是常数,但是还是太低效
●bitmap5:共140个优先级,一共140个进程队列,为提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样就可以大大提高查找效率

3.5.4过期队列
●过期队列何活动队列结构一模一样
●过期队列上放置的进程,都是时间片耗尽的进程
●当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
3.5.5active指针和expired指针
●active指针永远指向活动队列
●expired指针永远指向过期队列
●可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在
●在合适的时候,只要能交换active指针和expired指针的内容,就相当于具有了一批新的活动进程
3.5.6总结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,称之为进程调度O(1)算法
cpp
代码块
struct rq {
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned long nr_running;
unsigned long raw_weighted_load;
#ifdef CONFIG_SMP
unsigned long cpu_load[3];
#endif
unsigned long long nr_switches;
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
unsigned long nr_uninterruptible;
unsigned long expired_timestamp;
unsigned long long timestamp_last_tick;
struct task_struct *curr, *idle;
struct mm_struct *prev_mm;
struct prio_array *active, *expired, arrays[2];
int best_expired_prio;
atomic_t nr_iowait;
#ifdef CONFIG_SMP
struct sched_domain *sd;
/* For active balancing */
int active_balance;
int push_cpu;
struct task_struct *migration_thread;
struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;
/* sys_sched_yield() stats */
unsigned long yld_exp_empty;
unsigned long yld_act_empty;
unsigned long yld_both_empty;
unsigned long yld_cnt;
/* schedule() stats */
unsigned long sched_switch;
unsigned long sched_cnt;
unsigned long sched_goidle;
/* try_to_wake_up() stats */
unsigned long ttwu_cnt;
unsigned long ttwu_local;
#endif
struct lock_class_key rq_lock_key;
};
/*
* These are the runqueue data structures:
*/
struct prio_array {
unsigned int nr_active;
DECLARE_BITMAP(bitmap, MAX_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_PRIO];
};