Casual Note of OS

【注:本文内容大多为看到一些不错文章后的简单笔记,可作为了解或帮助回忆相关知识点的线索。但这些文章或内容虽然易懂但可能不太准确,建议还是看书!!】

 

 

计算机和OS启动过程:https://www.cnblogs.com/z-sm/p/5657914.html

CPU中断:https://www.cnblogs.com/z-sm/p/15162076.html

OS进程调度:https://www.cnblogs.com/z-sm/p/15200785.html

OS多路复用IO:https://www.cnblogs.com/z-sm/p/6680141.html

关于Linux的历史和内部原理的入门书籍,推荐阅读 Linux内核v0.1.2完全注释 一书,前面几章对Linux的发展过程、版本变化等做了详细的介绍。

 

20170104

冯诺依曼计算机(遵循冯诺依曼结构设计的计算机:存储器、运算器、控制器、输入设备、输出设备)之前也有计算机,不过在那之前的计算机是专用的,不可编程,只能干特定的事情没法干其他事。与之前计算机的一个不同在于冯诺依曼计算机是通用计算机可以干很多事情,并且程序是可存储的(“存储程序”),多个程序可以存储在计算机中。

 

20170328

1、典型的系统硬件组成结构

现代计算机组成中,IO bridge分为北、南桥两部分,分别称为MCH(Memory Controller Hub)、ICH(IO Controller Hub),分别用来管理高速(如内存等)、中低速组件(如磁盘、USB等)。

 

 

 

2、存储设备层次结构

 

 

 

典型 PC 机上通常含有三种类型的存储器,一种是用来运行程序和临时保存数据的内存存储器,一种是存放着系统开机诊断和初始化硬件程序(即BIOS)的 ROM,另一种是用来存放存计算机实时时钟信息和系统硬件 配置信息的少量 CMOS 存储器。

为什么叫主存(main memory)?因为CPU地址总线控制的不止是通常所认为的内存,还有显存等,这里面最常用的是通常认为的内存,故叫主存?

速度级别:

硬盘:毫秒级

内存:纳秒级

 

 3、OS提供的抽象表示

 

1、进程、线程。并行化:线程级并行(多核或超线程)、指令级并行、单指令多数据(SIMD)

2、虚拟存储器

 ————

(从上可见,栈从上向下增长,故亦称下推表)

 

 

 

20170427

精简指令集/复杂指令集的CPU架构:

RISC: MIPS架构PowerPC架构(IBM)、ARM架构(ARM)、SPARC架构(Sun,后被Oracle收购) 等的CPU用RISC

CISC:X86架构(Intel、AMD、VIA等) 等的CPU用CISC

题外话:Intel 1代为8位机(8080、8085)、2代16位机(8086)、3代为32位机(80386)、4代及目前最新为64位机。64位已经远远够用了,所以位长应该不会再增了。

 

20170428

计算机内部负整数为什么用补码?

原因是:只有这种形式,计算机才能实现正确的加减法。

计算机其实只能做加法,1-1其实是1+(-1)。如果用原码表示,计算结果是不对的。比如说:

1   -> 00000001

-1 -> 10000001

+ ------------------

-2 -> 10000010

用符合直觉的原码表示,1-1的结果是-2。

如果是补码表示:

1   -> 00000001

-1 -> 11111111

+ ------------------

0  ->  00000000

结果是正确的。

 

 

计算机浮点数运算为什么不准确?

如C下 printf("%.10f\n",0.1f*0.1f); 为0.0100000007,Java下 System.out.println(0.1f* 0.1f); 为0.010000001,虽然保留位数少的话可得期望值,但其实真正的计算结果有很多位,所以计算结果不是我们期望的。

原因:运算本身没错,而是计算机不能精确表示很多数,比如0.1,数都不能精确表示当然运算结果也不准确了。为什么不能精确表示呢——与十进制只能表示10的若干次方和的数一样,采用二进制的计算机只能表示2的若干次方和的数,前者不能准确表示无限小数如1/3,后者除不能表示无限小数外还有一些数也不能准确表示如0.1。

处理不精确:如果要求的精度不高,可以四舍五入;否则可以将小数转化为整数进行运算,算完再转化为小数。

 

 20170719

1、CPU视角看计算机启动过程(见 CPU阿甘——码农翻身

2、CPU视角看程序装载运行过程(见 CPU阿甘之烦恼——码农翻身):地址重定位、内存管理单元MMU,分页、工作集、页表、缺页中断,分段、段表、段错误

3、进程、线程实现(见 我是一个进程——码农翻身

4、死锁相关(见 我是一个线程——码农翻身

5、同步和锁

同步和互斥问题(见 那些烦人的同步和互斥问题——码农翻身):脱机打印、信号量、生产者消费者问题(通过信号量解决)

锁(见 编程世界的那把锁——码农翻身):CAS、TAS等操作 -> 自旋锁 -> 可重入锁 -> Semaphore、ReentrantReadWriteLock、CountDownLatch、CyclicBarrier
 
 
20170907
死锁的产生与解决:

1、产生死锁的原因主要是:
(1)因为系统资源不足。
(2)进程运行推进的顺序不合适。
(3)资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
2、产生死锁的四个必要条件:
(1)互斥条件:一个资源每次只能被一个进程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
  这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
3、死锁的预防:

(1)资源一次性分配:(破坏请求和保持条件)
(2)可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)
(3)资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏循环等待条件)

4、死锁的避免:(银行家算法)

预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法

5、死锁的检测:进程等待图(wait graph)看是否有环

6、死锁的解除(比上述几个知识点更重要):

(1)剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

(2)撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等

 
2017.09.08
进程间通信(IPC)的方式:
信号/信号量、无名/命名/高级管道、共享内存、消息队列、套接字。后两个是不同机器上的进程间通信方式,其他是同一个机器上不同进程间 或 同一进程内不同线程间的通信方式。
1 信号/信号量。一进程内不同线程间通信的方式。
信号(signal)用于通知接收进程某个事件已经发生。比如Linux IO多路复用中的epoll,在IO就绪时会通知进程。
信号量(semophore )是一个计数器,可用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
2 无名/命名/高级管道。一机器上不同进程间通信的方式。

无名管道(Pipe)是用于父子进程单向通信的特殊内存文件。管道是一种具有两个端点的通信通道,一个管道实际上就是只存在在内存中的文件,对这个文件操作需要两个已经打开文件进行,他们代表管道的两端,也叫两个句柄。管道是一种特殊的文件,不属于一种文件系统,而是一种独立的文件系统,有自己的数据结构,根据管道的使用范围划分为无名管道和命名管道。是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

命名管道(Named Pipe)是用于进程间单向通信的特殊文件(不限父子进程、文件在磁盘),也叫FIFO文件。是为了解决无名管道只能在父子进程间通信而设计的,命名管道是建立在实际的磁盘介质或文件系统(而不是只存在内存中),任何进程可以通过文件名或路径建立与该文件的联系,该文件的读写遵循FIFO原则,虽然FIFO文件的inode节点在磁盘上,但仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。

高级管道(popen)将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。

3、共享内存。一机器上不同进程间、一进程内不同线程间通信的方式。

​共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到,大大提高了进程间通信的速度。

最快的IPC方式。普通情况下,不同进程的虚拟地址空间即使一样,也会映射到不同的物理内存,从而两进程的数据读写互不影响。共享内存就是把不同进程各自的一段虚拟内存空间映射到同一段物理内存,这样一个进程写入东西其他进程立马可看到,从而大大提高了进程间通信速度。

共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
例子:OS调用IO设备时,IO驱动程序检查IO工作完成与否是通过内存的一些二进制位来互通有无的,这种通过标志位进行通信的方式OS内核中很常用;另外,OS中的内存映射文件技术也是一种共享内存应用的例子。

4 消息队列。不同机器上的进程间通信的方式。
OS内的消息队列:消息队列是由消息组成的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
更一般的消息队列:比如Kafka等消息队列。
5 套接字(socket)。不同机器上的进程间通信的方式。

与其他通信机制不同的是,套接字可用于不同机器间的进程通信。有Socket套接字(TCP/IP)、Unix域套接字等。 

套接字:Linux、Windows等平台,同一或不同Server上。
有名管道和共享内存:Windows,同一Server上。
Unix域套接字:Linux、Unix,同一Server上。
 
 
RAID(Redundant Arrays of Independent Disk,独立磁盘冗余阵列):RAID0、RAID1、RAID5、RAID10、RAID01、RAID50等
 
 
2017.09.11

页面置换算法(详见:页面置换算法]):把外设中的块数据加载入内存页的过程

总结:五种

最佳置换算法

基于队列:FIFO。实现简单,时间效率高;效果差(缺页多),Belady异常。

基于栈:LRU。与FIFO比:实现较复杂,时间效率低;效果好,没有Belady异常

LFU,与LRU类似

ClOCK:时间开销比LRU小但效果接近LRU

1、最佳置换算法(OPT)

置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现

示例:(9次缺页中断、6次页面置换)

2、先进先出置换算法(FIFO)

按页面中写入物理块数据的时间顺序排序,优先淘汰时间最早者。

基于队列的算法。优先淘汰最早进入内存的页面,亦即在内存中驻留时间最久的页面。该算法实现简单,只需把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面。但该算法与进程实际运行时的规律不适应,因为在进程中,有的页面经常被访问。FIFO算法还会产生当所分配的物理块数增大而页故障数不减反增的异常现象(由 Belady于1969年发现,故称为Belady异常)

示例:(15次缺页中断、12次页面置换)

3、最近最久未使用置换算法(LRU)

按页面被访问的时间顺序排序,优先淘汰时间最早者。

基于栈的算法。选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问。LRU性能较好,但需要寄存器和栈的硬件支持。LRU是栈类的算法。理论上可以证明,栈类算法不可能出现Belady异常。FIFO算法基于队列实现,不是栈类算法。

示例:(12次缺页中断、9次页面置换) 

基于优先队列实现LRU:假设优先级越高越不应被删除,则对于要访问的目标数据(访问的页)

若缓存(三个物理块)中存在,则从缓存中访问该页,并把该页置为最高优先级;若不存在,则从缓存移除优先级最低的(最长时间未使用的物理块中到)页、把新页加入缓存,并把新页面置为最高优先级。

基于LinkedHashMap实现 O(1) 读写时间复杂度的FIFO、LRU、LFU:实际上就是对上述基于优先队列方案的实现,LinkedHashMap的entry按加入的顺序保存,故“晚加入”相当于上面的“高优先级”。对于某个数据:

若LinkedHashMap中存在该元素,则访问删除该元素然后重新添加到map;若不存在,则直接加入(未满时)或 移除第一个entry并将元素加入(满时)到map。代码:

/**
 * 三种页面置换算法(FIFO、LRU、LFU)的实现。<br>
 * <br>
 * FIFO关键在于借助LinkedHashMap来实现队列的FIFO效果;<br>
 * LRU关键在于借助LinkedHashMap来实现按访问时间排序的效果;<br>
 * LFU关键在于借助LinkedHashSet来实现访问次数一样时按访时间排序的效果;<br>
 */
abstract class DbUtilWithCache<K, V> {
    public enum CacheType {
        /** 按存入的时间排序,越晚存入优先级越高 */
        FIFO,
        /** 按访问时间排序,越晚访问优先级越高 */
        LRU,
        /** 按访问次数排序,次数越多优先级越高 */
        LFU
    }

    private int capacity;
    private CacheType cacheType;

    // 以下LRU用到,必须用LinkedHashMap
    private LinkedHashMap<K, V> key2valueMap = new LinkedHashMap<>();// LinkedHashMap具有FIFO的特点,借助之来维护优先级:越晚存入(链表越往后)优先级越高

    // 以下LFU用到
    private Map<K, Integer> key2countMap = new HashMap<>();
    private Map<Integer, LinkedHashSet<K>> count2keysMap = new HashMap<>();// 关键在于借助LinkedHashSet来使得访问次数一样时按访问时间排序
    private int minCount = 0;

    public DbUtilWithCache(int capacity, CacheType cacheType) {
        this.capacity = capacity;
        this.cacheType = cacheType;
    }

    /** 根据该该key的缓存数据是否存在来决定进行添加或修改缓存数据。O(1)时间复杂度 */
    private void addOrUpdateCache(@NotNull K key, @NotNull V value) {
        switch (cacheType) {
        case FIFO:
        case LRU:
            if (!key2valueMap.containsKey(key)) {// add:添加数据
                if (key2valueMap.size() == capacity) {// 删除优先级最低者
                    Iterator<Map.Entry<K, V>> it = key2valueMap.entrySet().iterator();
                    key2valueMap.remove(it.next().getKey());
                }
                key2valueMap.put(key, value);
            } else {// update:更新值和优先级
                if (cacheType == CacheType.LRU) {
                    key2valueMap.remove(key);// FIFO和LRU的区别只在是否有这一行,先删再重新加可使得按访问时间排序
                }
                key2valueMap.put(key, value);
            }

            break;
        case LFU:
            if (!key2valueMap.containsKey(key)) {// add
                if (key2valueMap.size() == capacity) {// 删除优先级最低者
                    LinkedHashSet<K> keys = count2keysMap.get(minCount);
                    K tarKey = keys.iterator().next();
                    key2valueMap.remove(tarKey);
                    key2countMap.remove(tarKey);
                    keys.remove(tarKey);
                    if (keys.isEmpty()) {
                        count2keysMap.remove(minCount);
                    }
                }
                key2valueMap.put(key, value);
                int curCount = 1;
                key2countMap.put(key, curCount);
                count2keysMap.computeIfAbsent(curCount, LinkedHashSet<K>::new).add(key);
                minCount = curCount;
            } else {// update
                // lru.remove(key);//若用的是LinkedHashMap且加上这句,则数据会按访问时间排序,但由于LFU删除时是按访问频数删的,所以加这个并没什么用处
                key2valueMap.put(key, value);
                //
                int oldCount = key2countMap.get(key);
                int newCount = oldCount + 1;
                key2countMap.put(key, newCount);
                //
                LinkedHashSet<K> keys = count2keysMap.get(oldCount);
                keys.remove(key);
                if (keys.isEmpty()) {
                    count2keysMap.remove(oldCount);
                    if (minCount == oldCount) {
                        minCount = newCount;
                    }
                }
                count2keysMap.computeIfAbsent(newCount, LinkedHashSet<K>::new).add(key);
            }

            break;
        default:
            break;
        }
    }

    protected abstract V getValFromDB(K key);

    protected abstract void setValVoDB(K key, V val);

    /** 获取数据,不存在则返回null */
    public V get(@NotNull K key) {
        V value = key2valueMap.containsKey(key) ? key2valueMap.get(key) : getValFromDB(key);
        if (null != value) {
            addOrUpdateCache(key, value);// 改变优先级
        }
        return value;
    }

    /** 设置数据 */
    public void put(@NotNull K key, @NotNull V value) {
        if (capacity < 1) {
            return;
        }
        if (key2valueMap.containsKey(key) && Objects.equals(key2valueMap.get(key), value)) {
            addOrUpdateCache(key, value);// 改变优先级
        } else {
            setValVoDB(key, value);
            addOrUpdateCache(key, value);// 添加或修改数据
        }
    }
}
FIFO LRU LFU

其关键就在于LinkedHashMap的 Map+双向链表 功能:Map保证了O(1)时间复杂度的读取、插入、删除,双向链表保证了优先级。

当然,基于LinkedList也能实现,但时间复杂度不是O(1);基于Redis ZSet亦可实现,同样地,时间复杂度也不是O(1)

头条国际化后端社招一面算法题——O(1)时间复杂度和空间复杂度的LRUCache简单实现(HashMap + 双向链表,这实际上就是LinkedHashMap的内部实现原理):

 1 public class Main {
 2     public static void main(String[] args) {
 3         // Scanner input=new Scanner(System.in);
 4         // String str=input.next();
 5 
 6         LRUCache lRUCache = new LRUCache(2);
 7         lRUCache.put(1, 1); // 缓存是 {1=1}
 8         lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
 9         lRUCache.get(1);    // 返回 1
10         lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
11         lRUCache.get(2);    // 返回 -1 (未找到)
12         lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
13         lRUCache.get(1);    // 返回 -1 (未找到)
14         lRUCache.get(3);    // 返回 3
15         lRUCache.get(4);    // 返回 4
16 
17     }
18 
19 }
20 class Node{
21     public int val;
22     public Node pre, next;
23     public Node(int val){
24         this.val=val;
25         this.pre=null;
26         this.next=null;
27     }
28 
29 }
30 class LRUCache{
31     private Node head, tail;
32     private Map<Integer, Node> map;
33     private int cap, size;
34     public LRUCache(int cap){
35         this.head=new Node(-1);
36         this.tail=new Node(-1);
37         head.next=tail;
38         tail.pre=head;
39         this.map=new HashMap<>();
40         this.cap=cap;
41         this.size=0;
42     }
43     private void remove(Node tarNode){
44         Node pre=tarNode.pre;
45         Node next=tarNode.next;
46         pre.next=next;
47         if(null!=next){
48             next.pre=pre;
49         }
50     }
51     private void insetHead(Node tarNode){
52         Node first=head.next;
53         head.next=tarNode;
54         tarNode.pre=head;
55         tarNode.next=first;
56         if(null!=first){
57             first.pre=tarNode;
58         }
59     }
60     public int get(int key){
61         int res;
62         if(!map.containsKey(key)) res= -1;
63         else{
64             Node tarNode=map.get(key);
65             remove(tarNode);
66             insetHead(tarNode);
67             res= tarNode.val;
68         }
69         System.out.println(res);
70         return res;
71     }
72     public void put(int key, int val){
73         Node tarNode;
74         if(map.containsKey(key)){
75             tarNode=map.get(key);
76             tarNode.val=val;
77             //remove
78             remove(tarNode);
79         }else{
80             tarNode=new Node(val);
81             map.put(key, tarNode);
82             size++;
83         }
84         //insert
85         insetHead(tarNode);
86         //check
87         if(size>cap){
88             Node last=tail.pre;
89             remove(last);
90             map.remove(last.val);
91             size--;
92         }
93     }
94 }
View Code

真实应用场景,对象存储方向代理服务ReverseProxyController的文件下载及上传功能中加上LRU,使得若代理服务中存在文件缓存则不用去对象存储中下载。

4、最近最不常使用置换算法(LFU)

按页面被访问的次数排序,优先淘汰次数最少者。

与LRU类似,不过每次淘汰的是过去一定时期(看指定的是多长)内被访问次数最少的页

5、时钟置换算法(Clock)

LRU算法的性能接近于OPT,但是实现起来比较困难,且开销大;FIFO算法实现简单,但性能差。所以操作系统的设计者尝试了很多算法,试图用比较小的开销接近LRU的性能,这类算法都是CLOCK算法的变体。

 

 

20200617

操作系统文件缓存(Page Cache)

参阅:码农翻身-https://mp.weixin.qq.com/s/fZWgCfUZ_sNAFfAR2kyhiw

使用Page Cache的原因或作用:外存的读写速度远小于内存,根据局部性原理,一次IO时可以加载大于实际需要的数据(加载1Page,Linux通常是4kB大小、Windows通常是256KB大小)到内存,这样下次访问的数据很可能在上次访问的数据附近,这时就可减少IO次数、提高访问效率。

内核态内存中有真实的物理页,每个程序也有自己的虚拟页,虚拟页映射到内存页。

进程结束后该进程相关的Page Cache内存回收释放,因为很可能程序要再次运行,此时可减少IO。“最大限度地用尽Page Cache,直到达到上限才进行页面置换”。

数据从硬盘读入内存时是放到内核态的内存中(即放在Page Cache);内核态的内存是不会直接让用户程序访问的,所以还会有一次从内核态到用户态的内存复制!!可见,很费内存,如下图:

   、    

 

当然,也有手段来达到共享内存、减少内存占用的目的。如mmap系统调用(Memory-mapped files,java等很多语言也支持) ,原理图如下:

 

 

 另外,上述所说的是OS文件系统的缓存,并不是万金油,很多数据库为了提高效率会实现自己的缓存机制,而不是用OS文件系统的缓存。 

 

20170912

锁在OS底层的实现机制:

在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制;

OS基于这几个CPU硬件机制,就能够实现锁;

再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等等等等)。

 

20171012

关于fork()及父子进程间的关系:(可参考fork系统调用fork后子进程复制了父进程的什么

Linux中C/C++程序的内存布局包括text段、data段、bss段、堆段、栈段五部分(详见C/C++小记、42)。

fork()函数的实质是一个系统调用(和write函数类似),其作用是创建一个新的进程,当一个进程调用它完成后就出现两个几乎一模一样的进程。其中由fork()创建的新进程被称为子进程,而原来的进程称为父进程。子进程是父进程的一个拷贝——即子进程从父进程得到了数据段和堆栈的拷贝,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存方式进行访问。(代码段共享,数据段、堆、栈复制新的)。fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。 示例:

 1 #include <unistd.h>
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 
 5 int main(int argc,char *argv[])
 6 {
 7   pid_t pid;
 8   pid = fork();
 9   if(pid<0)
10     {
11     printf("fail to create!");
12     exit(1);
13     }
14   else if(pid==0)
15     printf("In child process, pid=%d\n",getpid());
16   else
17     printf("In parent process, pid=%d\n",getpid());
18   return 0;
19 }
20 
21 //输出:
22 In parent process, pid=12072
23 In child process, pid=12073
24 
25 
26 
27 /*这是一个调用fork()函数创建一个子进程,然后分别打印输出子进程和父进程中的变量的实例*/
28 #include <unistd.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <errno.h>
32 int glob = 6;
33 int main(int argc,char *argv[])
34 {
35     int var;    //内部变量
36     pid_t pid;      //文件标识符
37     var = 88;   //内部变量赋值
38     char  str[]="good";
39 
40     printf("创建新进程之前.\n");   //还没有创建子进程
41     if((pid=fork())<0)
42     {
43       perror("创建子进程失败!\n");
44     }
45     else if(pid==0)
46     {
47       glob++;
48       var++;
49     }
50     else
51     {
52       sleep(2); //父进程阻塞两秒
53     }
54     printf("进程标识符为=%d,glob=%d,var=%d,varAddr=%p,firstChAddr=%p\n",getpid(),glob,var,&var,str);//分别在子进程中输出两个变量的值
55     exit(0);
56 }
57 
58 //输出:
59 创建新进程之前.
60 进程标识符为=12318,glob=7,var=89,varAddr=0x7ffe75b705e8,firstChAddr=0x7ffe75b705f0
61 进程标识符为=12317,glob=6,var=88,varAddr=0x7ffe75b705e8,firstChAddr=0x7ffe75b705f0
View Code

子进程从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、计时器等。因此可以看出,使用fork系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段里的绝大部分内容,使得fork系统调用的执行速度并不很快。 

在Linux中,对fork进行了优化,调用时采用写时复制 (COW,copy on write)的方式,在系统调用fork生成子进程的时候,不马上为子进程复制父进程的资源,而是在遇到“写入”(对资源进行修改)操作时才复制资源。

 

20180806

为什么分支预测能提高执行效率?

一条CPU指令的完整执行可能包括取指(Fetch)、解码(Decode)、执行(Execute)、写回(Write-back)。对于指令序列ABCDE,CPU不是依次完整执行完每条指令的各个阶段再执行下一条,而是流水线执行的:指令A处于执行阶段时可能指令B处于解码指令C处于取指。

分支预测:然而若A执行完后是需要跳转到D的,则执行A时B的解码、C的取指就会白做。若有分支预测,则在执行A时流水线执行的就可能是D、E而不是B、C,从而避免白做,提高效率。

示例:(如下求和排序前所用时间是排序后的六倍左右)

    public static void main(String[] args) throws URISyntaxException {
        // Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c)
            data[c] = rnd.nextInt() % 256;

        // !!! With this, the next loop runs faster
        Arrays.sort(data);

        // Test
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i) {
            // Primary loop
            for (int c = 0; c < arraySize; ++c) {
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
View Code

 

参考:https://stackoverflow.com/questions/11227809/why-is-it-faster-to-process-a-sorted-array-than-an-unsorted-array

 

20190530

操作系统发展历程

人工OS -> 单道批处理OS -> 多道批处理OS -> 分时OS -> 实时OS

参考:https://www.cnblogs.com/enochzzg/p/9997978.html

 

 

20191122

不同操作系统(OS)所用的文件系统(FS)格式:

Windows:NTFS

Linux:ext4

OSX:HFS+

Solaris and Unix:ZFS

通用(最早出现,各种OS都支持):FAT32(32位,文件最大4GB)、extFAT(extended fat32,64位,单文件最大16EB)。MS Dos就用这种格式,可参阅:https://zhuanlan.zhihu.com/p/25992179

 

注:不同OS并不是只支持自己的默认FS,如也支持读NTFS的文件

分区与分区表:

分区:一个硬盘上可以有多个文件系统同时存在,即通过对硬盘分区(partition)每个区可以有自己的文件系统格式。硬盘必须先分区,才能指定每个区的文件系统。

分区表:分区大小、起始位置、结束位置、文件系统等信息,都存在分区表里。分区表有MBR(Master Boot Record)、GPT(Globally Unique Identifier Partition Table)两种格式:

MBR是传统格式,兼容性好;但一个分区最大2TB容量、最多 4个主分区 或 3个主分区+1个扩展分区+无限逻辑分区;

GPT更现代,功能更强大,一个分区最大18EB、最多128个分区。一般来说,都推荐使用 GPT。

MBR和GPT的区别可参阅:https://zh.wikipedia.org/wiki/%E4%B8%BB%E5%BC%95%E5%AF%BC%E8%AE%B0%E5%BD%95

 

UEFI(Unified Extensible Firmware Interface,统一的可扩展固件接口)

是由Intel发起制定的一个接口标准,抽象了操作系统与系统固件之间的交互,作为BIOS的替代方案,是的操作系统不与系统硬件绑定。

详情可参阅:https://zhuanlan.zhihu.com/p/25281151

 

20191127

单线程模型:javascript、python异步编程、redis、nginx都是单线程模型来达到高并发能力,在这种模型下没有现存竞争所以不要考虑资源访问的竞争问题,而单线程模型在实现上通常用事件驱动机制。

注:可见万变不离其宗

 

20200218

磁盘一次IO的时间:约10ms(忽略传输时间)

磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在0.x 毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右。

20200528

CPU提高执行效率的措施:

缓存:使用缓存的理论基础是基于局部性原理,有一级、二级、三级缓存。这里面涉及到缺页中断、页面调度等过程,见前文。

流水线(乱序执行):多条指令的 取指、译码、执行、回写 四个过程有些可以并行

分支预测:见前文

这些措施虽然提高了CPU运行效率,但由于预测失败时数据已被缓存了,可能导致有漏洞被恶意利用,具体可参阅 https://mp.weixin.qq.com/s/XEDCCQntIRghWhfigciWSA

 

20200902

密码保存方式的演变:

明文保存:缺点是容易通过彩虹表找出,数据库被攻破则密码就好明文泄露则裸奔)

密码加salt后通过hash function(如SHA-256)算得hash值,hash值及salt一起保存:缺点是现代硬件算力越来越强,仍可通过碰撞检测攻破

在hash function上做文章,加入work factor来调节hash function计算时对CPU、内存的消耗,硬件越好factor越大:如SpringSecurity中的BcryptPasswordEncoder就是采用这种方法。

 

 

 

20221204 进程调度(CPU上下文切换)

上下文切换(即进程调度)“切换”了什么?为什么“切换”耗时?

从一个进程切换到另一个进程需要保存前者的执行“现场”,包括保存寄存器的值和内存映射、更新不同的表格和列表、清除和重新调入内存高速缓存等。这种切换称作 进程间切换(process switch) 和 上下文切换(context switch)

进程切换时需要消耗一定的CPU周期,用于包括检查中断、内存映射、清除和重新调入高速缓存等。如果进程间的切换时间需要 1ms,再假设时间片设为 4 ms,那么 CPU 在做完 4 ms 有用的工作之后,将花费 1 ms 进行进程间的切换。此时CPU 的时间片会浪费 20% 的时间在管理开销上,显然利用率很低。

可见,时间片长短的设置也会影响CPU的时间利用率,设置得太短会导致过多的进程切换并降低 CPU 时间利用率、设置太长会导致一个短请求很长时间得不到响应。时间片的长度设为Timer周期的整数倍,通常是 20 - 50 毫秒之间。

CPU进行上下文切换(进行进程调度)的主要原因

从任务执行的角度看:分给当前任务的时间片用完(不管任务是否执行完)、时间片没用完但【任务执行完了,或任务因IO等操作阻塞了】,其实两者分别对应了抢占式、非抢占式调度。

从切换的CPU实现角度看:硬件中断(比如发起IO等系统调用时通常会通过int n触发硬件中断,使CPU在用户态和内核态间的上下文切换)等

进程调度的考虑因素、调度时机、调度算法

(更多详情可参阅《一篇文章带你「重新认识」线程上下文切换怎么玩儿》)

进程调度是由时钟中断驱动的,根据响应时钟中断时是否可打断正在执行的任务可将调度算法分为抢占式调度(preemptive)和非抢占式调度(nonpreemptive)两类。后者CPU在一个进程执行完或阻塞后才会执行下一个进程。

通常有批处理系统、分时系统(交互系统)、实时系统三种系统,不同类型的系统所用的调度算法有所差别。关键点在于理解调度发生的时机及如何选下一个任务

批处理系统

1 what:为了解决人机矛盾及CPU和I/O设备之间速度不匹配的矛盾,出现了批处理系统。批处理指用户将一批作业提交给操作系统后就不再干预,由操作系统控制它们自动运行,这种采用批量处理作业技术的操作系统称为批处理操作系统。批处理操作系统分为单道批处理系统和多道批处理系统,区别在于内存中是否仅存放一个作业。批处理操作系统不具有交互性,它是为了提高CPU的利用率而提出的一种操作系统。

2 调度考虑的因素:吞吐量(单位时间内完成的作业数)、周转时间(从提交任务到得到结果的时间)

3 基本调度时机:在执行的任务执行结束或阻塞

4 调度算法(非抢占式):

a FIFO:按提交的一批任务里任务的顺序分别执行。简单但非最优。

b 最短任务优先:提交的一批任务按预测执行时间升序排序后的顺序依次执行这些任务,这样各任务总的周转时间最短。理论依据是正序和>乱序和>反序和,如四个任务预计耗时2 4 5 6,则总等待时间 2*4+4*3+5*2+6*1=36。

c 最短剩余时间优先(最短任务优先的抢占式版本):每次时钟中断时都检查是否有所需运行时间小于当前运行任务剩余运行时间的任务,有则转去执行该任务。

分时系统(交互系统)

1 what:把处理器的运行时间分成很短的时间片,按时间片轮流把处理器分配给各作业使用。若某个作业在分配给它的时间片内不能完成其计算,则该作业暂时停止运行,把处理器让给其他作业使用,等待下一轮再继续运行。由于计算机速度很快,作业运行轮转得很快,给每个用户的感觉好像是自己独占一台计算机。

2 调度考虑的因素:响应时间(从指令执行开始到得到结果的时间)、预期满足性(proportionality,一个任务的实际响应时间与用户心理预期的差异。如当认为一个请求很复杂需要较多时间时用户会认为很正常并且可以接受,但是一个很简单的程序却花费了很长的运行时间用户就会很恼怒)

3 基本调度时机:在执行的任务执行结束或阻塞,或时间片结束(比批处理者多的一个)

4 调度算法(抢占式):

轮询调度:每个任务都被分配固定长度的CPU执行时间片,各任务轮流执行。若【在执行的任务执行结束或阻塞,或时间片结束】则让出CPU(可见与批处理的FIFO类似,只不过多了根据时间片的调度)。最古老、最简单、最公平并且最广泛使用,各进程平等。

优先级调度:每个任务都被分配固定长度的CPU执行时间片以及一个优先级,每次调度都选优先级最高的任务执行,且在时钟中断时降低当前执行任务的优先级。若【在执行的任务执行结束或阻塞,或时间片结束、或存在比当前任务优先级更高的任务】则让出CPU。

任务优先级可通过Unix下的 nice 手动调整,也会由OS自动调整。对于后者,通常将优先级设为一个时间片内CPU利用率的倒数(如4ms时间片内花了1ms在IO引起的上下文切换上,则优先级为4/(4-1)≈1.3),即优先给CPU利用率低的任务执行机会,其原理是让CPU需求小的任务尽快执行掉,类似于上面批处理的最短任务优先。

通常是优先级+轮询调度组合一起,即把同优先级的任务归为一组,组间用优先级调度、组内用轮询调度。缺点是低优先级内的任务可能很难分到执行机会。

其他的,最短任务优先(预测执行时间,选择最小时间的任务)、保证调度、彩票调度、公平分享调度(进一步考虑进程的拥有者使各用户获得CPU执行机会相等)等,详见节首文章。

实时系统

1 what:为了能在某个时间限制内完成某些紧急任务而不需时间片排队,诞生了实时操作系统。这里的时间限制可以分为硬实时系统和软实时系统两种情况:如果某个动作必须绝对地在规定的时刻(或规定的时间范围)发生,则称为硬实时系统。例如,飞行器的飞行自动控制系统,这类系统必须提供绝对保证,让某个特定的动作在规定的时间内完成。如果能够接受偶尔违反时间规定,并且不会引起任何永久性的损害,则称为软实时系统,如飞机订票系统、银行管理系统。

2 调度考虑的因素:最后截止时间、可预测性

3 调度算法:在这两种情形中,实时都是通过把程序划分为一组进程而实现的,其中每个进程的行为是可预测和提前可知的。这些进程一般寿命较短,并且极快的运行完成。在检测到一个外部信号时,调度程序的任务就是按照满足所有截止时间的要求调度进程。

 

20221126

CPU usage(利用率) 与 CPU load(负载)的含义和区别:(可参阅这篇文章

前者体现CPU的利用情况(工作饱不饱和)、后者体现CPU的工作量情况。利用率与负载率没有必然练习。

利用率usage:一段时间内包含的若干个CPU时间片中,可能用来执行进程也可能不执行进程在那空闲,用来执行进程的时间片的占比就是CPU利用率。有两个要求:尽可能准确、要能整体体现瞬时CPU利用情况。实际如何做到的呢?

如何尽可能准确?这样计算:  sum(用于执行进程的时间片数)/(统计时长的时间片数 * 核数) 。存在的问题:统计时长不好确定且容易受时间起点的影响、无法确定瞬时利用率。

如何体现瞬时状态?这样计算: 任意时刻在运行进程的核数/总核数 。缺点:结果极易受所选时刻大波动,如3/4 -> 0。

Linux 的 top怎么做的?两者结合:在采样周期(较大,默认3s)内按小计算周期(较小,默认1ms)计算若干个瞬时利用率累加起来,平均值当最终的利用率。详情可参阅文章 开发内功修炼——Linux中CPU利用率如何计算出来

负载load:一段时间内各CPU正在执行的进程及等待这些CPU执行的进程数的和的统计信息,也即CPU进程队列的长度的统计信息(即有多少任务需要CPU执行)。更简单理解,负载表示的是没被CPU执行完(正在执行和尚未执行)的任务的统计信息。这个数字越小越好。

如果load average值长期大于系统CPU的个数则说明CPU很繁忙,负载很高,可能会影响系统性能,导致系统卡顿响应时间长等等。

一般业界能够被接受的值是 load average <= CPU核数 *0.7。 但现在硬件越来越便宜,核数庞大的机器也越来越大,如遇到机器的CPU核数很大,那么剩余的30%部分也越大,这个时候可以适当的调整下,只要不要所有核数都用满。

例子:高速公路收费站10个车道,那当有1-9辆车在不同的通道通过时,认为收费站的load<1;当正好10辆车在不同的通道时,load=1;当超过10辆车(假设每个通道是均匀有车)时,load>1.假设有100辆车,每个通道10辆,那就说明能有10辆车能过去,另外90辆车则需要等待。此时收费站的load为100/10=10. 这个10的负载表示系统当前满负荷运转,且还有相当于90%的满负载的请求在等待。

关系:Load大于1时CPU usage一般都是100%,要了解系统的压力,load比usage有意义。

如果机器持续在一段时间内都是load很高,那么也许机器性能下降了,可能需要进一步排查问题,也许是要增加机器了; 如果Load很高但系统的CPU使用率却比较低。可能的原因是频繁的上下文切换导致耗费了大量的CPU时间,以至于用在运算的CPU时间片比较少,却有很多进程在等待运行。

关于 Linux top 命令的输出信息含义,可参阅这篇文章。 

 

20230309

CPU组成:控制器(取指、译码、MMU)、运算器(算术逻辑运算,ALU)、寄存器(程序计数器PC、地址寄存器AR(基址、变址寄存器)、数据寄存器DR、指令寄存器IR、累加寄存器AC、程序状态寄存器PSW 六类)

 

 (关于CPU的简介,详可参阅 https://mp.weixin.qq.com/s?__biz=MzkwMDE1MzkwNQ==&mid=2247496057&idx=1&sn=19bb9b943e0d99a6a0d5a546b367e298&source=41

 

内存组成:内存由一系列的IC电路组成,每个IC电路包含 电源、控制、数据、地址 信号引脚

 

内存读写数据过程:假设我们要向内存 IC 中写入 1byte 的数据的话,它的过程是这样的:

首先给 VCC 接通 +5V 的电源,给 GND 接通 0V 的电源,使用 A0 - A9 来指定数据的存储场所,然后再把数据的值输入给 D0 - D7 的数据信号,并把 WR(write)的值置为 1,执行完这些操作后,即可以向内存 IC 写入数据

读出数据时,只需要通过 A0 - A9 的地址信号指定数据的存储场所,然后再将 RD 的值置为 1 即可。

图中的 RD 和 WR 又被称为控制信号。其中当WR 和 RD 都为 0 时,无法进行写入和读取操作。

 (关于内存的简介,详可参阅 https://mp.weixin.qq.com/s?__biz=MzkwMDE1MzkwNQ==&mid=2247496056&idx=1&sn=12e4550537bf1b7f4d60844898fd1dbd&source=41

机械硬盘组成

盘面、柱面,交叉得到磁道

一个磁道由若干个扇区组成,每个 513B,沿磁道方向若干个扇区组成簇、沿圆和圆心方向若干个扇区组成扇面。

 

需要注意,固态硬盘(Solid State Disk, SSD)不是磁盘,固态硬盘没有可移动的磁头构造、外形也不像唱片,与磁盘唯一的相似之处是即使电源关它存储的数据闭也不会丢失。

 

 

20230310

函数调用时栈帧里有哪些内容?

栈帧中有两个指针:栈基址指针、栈顶指针

调用创建栈帧,放入:主调方的栈基址、返回地址(通过程序计数器知道)、PSW(程序状态字)寄存器内容、被调函数的实参列表、被调函数的局部变量等

 

20240405

CPU地址线并不是只寻址内存RAM,还可寻址:

memory-maped IO设备

ROM,比如bios程序。对于intel CPU,计算机启动第一步就是CPU从oxfffff0位置执行代码,该位置是bios程序。

 

posted @ 2017-01-04 09:49  March On  阅读(410)  评论(0编辑  收藏  举报
top last
Welcome user from
(since 2020.6.1)