0%

2020年5月6日 上午11:53

首先说说ARM编程的一个思路改变:

  • 我们常见的上下级封装的接口调用系统,在这种系统下,我们上层调用是不关心下层是如何实现的,你调用完成功能就完了
  • 而ARM编程,由于接触到计算机的底层知识,无法有效的保持这种完美的抽象封装。所以,就导致了我们需要从cpu、内存的角度去反推上层的汇编、c如何编写,如何烧入代码,烧到哪里
    • 如何烧代码 + 代码逻辑功能 + 硬件内存地址=》三者耦合在一起

详细说说ARM的逆向思路

  1. 首先,我们应该尽可能的使用相对地址,因为这样可以让代码逻辑功能与硬件内存解耦,做到无论代码你烧到内存的那个位置都可以正确的执行。
  2. 但是,有些情况必须使用绝对地址才可完成的情况
    1. 相对地址不确定:比如说我就要使用内存中指定位置的数据,这个限制往往是ARM板硬件上的规定,也可以理解成限制。
    2. 相对地址太长
  3. 既然我们必须使用绝对地址,那么我们需要小心什么、做到什么才可以让程序正确的执行,也就是对反向限制方面:
    1. 链接地址要指定为内存中的地址(绝对地址)
    2. 烧的时候,要按链接地址烧入(绝对地址=链接地址)
    3. 已近烧入的代码,在执行拷贝代码动作的时候,也需要使用绝对地址(内存中的地址)
  4. 如果不满足这样天然的反向限制,程序就会出错。

2020年5月6日 上午10:59

伪指令ADR和LDR的区别_嵌入式_墨的博客-CSDN博客

  1. 位置无关编码 + 位置有关编码
    1. 一个事实:大部分指令是位置有关编码
      1. 位置无关编码(PIC,position independent code):汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关。
      2. 位置有关编码:汇编源码编码成二进制可执行程序后和内存地址是有关的。
    2. 我们在设计一个程序时,会给这个程序指定一个运行地址(链接地址)。就是说我们在编译程序时其实心里是知道我们程序将来被运行时的地址(运行地址)的,而且必须给编译器链接器指定这个地址(链接地址)才行。最后得到的二进制程序理论上是和你指定的运行地址有关的,将来这个程序被执行时必须放在当时编译链接时给定的那个地址(链接地址)下才行,否则不能运行(就叫位置有关代码)。但是有个别特别的指令他可以跟指定的地址(链接地址)没有关系,也就是说这些代码实际运行时不管放在哪里都能正常运行。
    3. 对比:
      1. 位置无关代码要好一些,适应性强,放在哪里都能正常运行;位置有关代码就必须运行在链接时指定的地址上,适应性差。位置无关码有一些限制,不能完成所有功能,有时候不得不使用位置有关代码。
  2. 链接地址和运行地址:可能相同也可能不同
    1. 对于位置有关代码来说:最终执行时的运行地址和编译链接时给定的链接地址必须相同,否则一定出错。裸机程序中,Makefile中用 -Ttext 0x0 来指定链接地址是0x0。这意味着我们认为这个程序将来会放在0x0这个内存地址去运行。但是实际上我们运行时的地址是0xd0020010(我们用dnw下载时指定的下载地址)。这两个地址看似不同,但是实际相同。这是因为S5PV210内部做了映射,把SRAM映射到了0x0地址去。
    2. 分清楚这两个概念:
      1. 链接地址:链接时指定的地址(指定方式为:Makefile中用-Ttext,或者链接脚本)
      2. 运行地址:程序实际运行时地址(指定方式:由实际运行时被加载到内存的哪个位置说了算)
  3. 从源码到可执行程序的步骤:预编译、编译、链接、strip
    1. 预编译:预编译器执行。譬如C中的宏定义就是由预编译器处理,注释等也是由预编译器处理的。
    2. 编译: 编译器来执行。把源码.c .S编程机器码.o文件。
    3. 链接:链接器来执行。把.o文件中的各函数(段)按照一定规则(链接脚本来指定)累积在一起,形成可执行文件。
    4. strip: strip是把可执行程序中的符号信息给拿掉,以节省空间。(Debug版本和Release版本)
    5. objcopy:由可执行程序生成可烧录的镜像bin文件。
  4. 程序段的概念:代码段、数据段、bss段(ZI段)、自定义段
    1. 段就是程序的一部分,我们把整个程序的所有东西分成了一个一个的段,给每个段起个名字,然后在链接时就可以用这个名字来指示这些段。也就是说给段命名就是为了在链接脚本中用段名来让段站在核实的位置。段名分为2种:一种是编译器链接器内部定好的,先天性的名字;一种是程序员自己指定的、自定义的段名。
    2. 先天性段名:
      1. 代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西
      2. 数据段:(.data),数据段就是C语言中有显式初始化为非0的全局变量
      3. bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,对应C语言中初始化为0的全局变量。
    3. 后天性段名:
      1. 段名由程序员自己定义,段的属性和特征也由程序员自己定义。
    4. 分析一些问题,跟这里结合,然后试图明白一些本质:
      1. C语言中全局变量如果未显式初始化,值是0。本质就是C语言把这类全局变量放在了bss段,从而保证了为0
      2. C运行时环境如何保证显式初始化为非0的全局变量的值在main之前就被赋值了?就是因为它把这类变量放在了.data段中,而.data段会在main执行之前被处理(初始化)。
  5. adr与ldr伪指令的区别
    1. ldr和adr都是伪指令,区别是ldr是长加载、adr是短加载。
    2. 重点:
      1. adr指令加载符号地址时,加载的是运行时地址;ldr指令在加载符号地址时,加载的是链接地址。
    3. 深入分析:只要知道adr和ldr分别用于加载运行地址和链接地址,从而可以判断是否需要重定位即可;根本不需知道为什么adr和ldr是这样子,但是我们还是给大家扩展讲下为什么adr和ldr可以加载不同的地址。
    4. 汇编LDR指令_网络_JadyC的博客-CSDN博客

2020年5月6日 上午9:30

参考:

位置有关码和位置无关码详细解释_C/C++_shenlong1356的博客-CSDN博客
位置无关码、位置有关码、链接地址、加载地址_运维_漫不经心-CSDN博客
伪指令ADR和LDR的区别_嵌入式_墨的博客-CSDN博客
汇编LDR指令_网络_JadyC的博客-CSDN博客

1.核心概念总结

  1. 在移植 uboot 时,接触到一个概念叫做 位置无关码,那么与它对应的就是位置有关码。提到这两个概念就还得提一提链接地址、运行地址。
    1. 链接地址,链接脚本里指定的,理论上程序运行时所处的地址。在编译时,编译器会根据链接地址来翻译位置有关码。
    2. 运行地址,程序运行时,实际所处的地址。
  2. 位置无关码,位置有关码,是相对于一条指令的正常目的来说的。
    1. 比如 ldr r0 ,=标号,它的正常目的是取得标号处的地址,对于这个目的,它是位置有关码,运行的地址不对就获取不到正确的标号地址,其实它无论在哪都是获取的程序运行地址等于链接地址时,标号的地址,如果你就是想要这个值,那么用这条指令是非常正确的,就不用理会什么位置无关码,位置有关码的概念了,这一点非常重要。
    2. 因此,当运行地址不等于链接地址时,并不是不可以用位置无关码,而是要看你用位置无关码是否达到了你想要的目的。
  3. 位置无关码,依赖于程序当前运行的PC值,进行相对的跳转,导致的结果就是,无论代码在哪,总能达到指令的正常目的,因此是位置无关的。
  4. 位置有关码,不依赖当前PC值,是绝对跳转,只有程序运行在链接地址处时,才能达到指令的正常目的,因此是位置有关系的。

2.从代码实践分析

下面,我们来看常用的汇编指令以及C语言中哪些操作是位置有关码,哪些是位置无关码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 汇编
SECTIONS {
. = 0x33f80000;
.text : { *(.text) }

. = ALIGN(4);
.rodata : {*(.rodata*)}

. = ALIGN(4);
.data : { *(.data) }

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(COMMON) }
__bss_end = .;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 汇编
.text
.global _start
_start:

bl close_watch_dog /* 相对跳转,位置无关 */
bl _start
adr r0, close_watch_dog /* 获取标号地址,位置无关 */

ldr r0, SMRDATA /* 获取标号处的值,位置无关 */

ldr r0, =0x12345678
ldr r0, =SMRDATA /* 获取标号地址,位置有关 */
ldr r0, =main /* 获取函数名的地址,位置有关 */
ldr r0 ,=__bss_start /* 获取链接脚本里标号的地址,位置有关 */


close_watch_dog:
mov r1, #0
str r1, [r0]
mov pc, lr

SMRDATA:
.word 0x22111120
1
2
3
4
5
6
7
8
9
10
11
int a;
void abc(){
a = 2;
}
int main(){
int b;
a =1 ;
b =1 ;
abc();
return 0;
}
编译后的反汇编代码:
  1. 编译后的反汇编代码如下,0x33f80000代表链接地址
  2. 如果我们把代码加载到0x33f80000处运行,此时运行的地址和链接地址完全一样,也就是下面的反汇编图
  3. 此时,链接地址 = 运行地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    // 反汇编
    33f80000 <_start>:
    33f80000: eb000006 bl 33f80020 <close_watch_dog>
    33f80004: ebfffffd bl 33f80000 <_start>
    33f80008: e28f0010 add r0, pc, #16
    33f8000c: e59f0018 ldr r0, [pc, #24] ; 33f8002c <SMRDATA>
    33f80010: e59f0018 ldr r0, [pc, #24] ; 33f80030 <SMRDATA+0x4>
    33f80014: e59f0018 ldr r0, [pc, #24] ; 33f80034 <SMRDATA+0x8>
    33f80018: e59f0018 ldr r0, [pc, #24] ; 33f80038 <SMRDATA+0xc>
    33f8001c: e59f0018 ldr r0, [pc, #24] ; 33f8003c <SMRDATA+0x10>

    33f80020 <close_watch_dog>:
    33f80020: e3a01000 mov r1, #0
    33f80024: e5801000 str r1, [r0]
    33f80028: e1a0f00e mov pc, lr

    33f8002c <SMRDATA>:
    33f8002c: 22111120 andscs r1, r1, #8
    33f80030: 12345678 eorsne r5, r4, #125829120 ; 0x7800000
    33f80034: 33f8002c mvnscc r0, #44 ; 0x2c
    33f80038: 33f80064 mvnscc r0, #100 ; 0x64
    33f8003c: 33f800a0 mvnscc r0, #160 ; 0xa0

    33f80040 <abc>:
    33f80040: e52db004 push {fp} ; (str fp, [sp, #-4]!)
    33f80044: e28db000 add fp, sp, #0
    33f80048: e59f3010 ldr r3, [pc, #16] ; 33f80060 <abc+0x20>
    33f8004c: e3a02002 mov r2, #2
    33f80050: e5832000 str r2, [r3]
    33f80054: e28bd000 add sp, fp, #0
    33f80058: e8bd0800 pop {fp}
    33f8005c: e12fff1e bx lr
    33f80060: 33f800a0 mvnscc r0, #160 ; 0xa0

    33f80064 <main>:
    33f80064: e92d4800 push {fp, lr}
    33f80068: e28db004 add fp, sp, #4
    33f8006c: e24dd008 sub sp, sp, #8
    33f80070: e59f3024 ldr r3, [pc, #36] ; 33f8009c <main+0x38>
    33f80074: e3a02001 mov r2, #1
    33f80078: e5832000 str r2, [r3]
    33f8007c: e3a03001 mov r3, #1
    33f80080: e50b3008 str r3, [fp, #-8]
    33f80084: ebffffed bl 33f80040 <abc>
    33f80088: e3a03000 mov r3, #0
    33f8008c: e1a00003 mov r0, r3
    33f80090: e24bd004 sub sp, fp, #4
    33f80094: e8bd4800 pop {fp, lr}
    33f80098: e12fff1e bx lr
    33f8009c: 33f800a0 mvnscc r0, #160 ; 0xa0

    Disassembly of section .bss:

    33f800a0 <a>:
    33f800a0: 00000000 andeq r0, r0, r0
假设拷贝到内存之后对应的地址
  1. 如果我们把代码加载到0x00000000处运行,此时实际运行的情况应该是下面这样的:此时链接地址≠运行地址
  2. 注意:下面的不是反汇编的到的,反汇编得到的还是上面那样的,下面的只是我们根据实际运行情况描绘的
  3. 0x00000000代表的是实际运行地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    // 反汇编
    00000000 <_start>:
    00000000: eb000006 bl 33f80020 <close_watch_dog>
    00000004: ebfffffd bl 33f80000 <_start>
    00000008: e28f0010 add r0, pc, #16
    0000000c: e59f0018 ldr r0, [pc, #24] ;
    00000010: e59f0018 ldr r0, [pc, #24] ;
    00000014: e59f0018 ldr r0, [pc, #24] ;
    00000018: e59f0018 ldr r0, [pc, #24] ;
    0000001c: e59f0018 ldr r0, [pc, #24] ;

    00000020 <close_watch_dog>:
    00000020: e3a01000 mov r1, #0
    00000024: e5801000 str r1, [r0]
    00000028: e1a0f00e mov pc, lr

    0000002c <SMRDATA>:
    0000002c: 22111120 andscs r1, r1, #8
    00000030: 12345678 eorsne r5, r4, #125829120 ; 0x7800000
    00000034: 33f8002c mvnscc r0, #44 ; 0x2c
    00000038: 33f80064 mvnscc r0, #100 ; 0x64
    0000003c: 33f800a0 mvnscc r0, #160 ; 0xa0

    00000040 <abc>:
    00000040: e52db004 push {fp} ; (str fp, [sp, #-4]!)
    00000044: e28db000 add fp, sp, #0
    00000048: e59f3010 ldr r3, [pc, #16] ; 33f80060 <abc+0x20>
    0000004c: e3a02002 mov r2, #2
    00000050: e5832000 str r2, [r3]
    00000054: e28bd000 add sp, fp, #0
    00000058: e8bd0800 pop {fp}
    0000005c: e12fff1e bx lr
    00000060: 33f800a0 mvnscc r0, #160 ; 0xa0

    00000064 <main>:
    00000064: e92d4800 push {fp, lr}
    00000068: e28db004 add fp, sp, #4
    0000006c: e24dd008 sub sp, sp, #8
    00000070: e59f3024 ldr r3, [pc, #36] ; 33f8009c <main+0x38>
    00000074: e3a02001 mov r2, #1
    00000078: e5832000 str r2, [r3]
    0000007c: e3a03001 mov r3, #1
    00000080: e50b3008 str r3, [fp, #-8]
    00000084: ebffffed bl 33f80040 <abc>
    00000088: e3a03000 mov r3, #0
    0000008c: e1a00003 mov r0, r3
    00000090: e24bd004 sub sp, fp, #4
    00000094: e8bd4800 pop {fp, lr}
    00000098: e12fff1e bx lr
    0000009c: 33f800a0 mvnscc r0, #160 ; 0xa0

代码分析:

一、BL指令
1
2
bl  close_watch_dog   //*位置无关码*
33f80000 : eb000006 bl 33f80020
  1. b 是相对跳转:跳转地址 = PC (PC=当前地址+8)+ 偏移值
  2. 偏移值:机器码 0xeb000006 低 24位 0x000006 按符号为扩展为 32 位 0x00000006 正数,向后跳转 0x6 * 4(32位位4字节)=(24)字节 也就是 0x18
  3. 起始运行地址为0x0000000时: 0x00000000(当前运行地址) + 8 + 0x18(偏移地址) = 0x00000020 跳转到正确位置
  4. 起始运行地址为0x3ff80000时: 0x3ff80000(当前运行地址) + 8 + 0x18(偏移地址) = 0x33f80020 跳转到正确位置
    二、ADR
    1
    2
    adr r0, close_watch_dog     /* 获取标号处的地址,*位置无关码*,伪指令 */
    33f80008: e28f0010 add r0, pc, #16
  5. 跳转地址 = PC (PC=当前地址+8)+ 偏移值
  6. 1、运行地址为0: 0 + 8 + 16 = 0x20 正确
  7. 2、运行地址为0x3ff80000: 0x3ff80008 + 8 + 16 = 0x33f80020 正确
  8. Adr 获取的是标号处的“实际”地址,标号在哪就是哪个地址,跟位置无关,总能获得想要的值。
    三、LDR
    LDR-1
    1
    2
    Ldr r0, SMRDATA       /* 获取标号处的值,*位置无关* *伪指令和下面的中间加上=号的不同**/
    33f8000c:e59f0018 ldr r0, [pc, #24];
  9. 起始运行地址地址0: r0 = c + 8 + 24 = 0x2c 处 22111120
  10. 起始运行地址0x3ff80000: r0 = 0x3ff8000c + 8 + 24 = 0x33f8002c 处 22111120 和上面得到的一样,所以和位置无关
    LDR-2
    1
    2
    Ldr r0, =SMRDATA     /* 获取标号地址,*位置有关* *这个中间加了= 不是伪指令,和上面的不同* */
    33f80014: e59f0018ldrr0, [pc, #24]; 33f80034 <SMRDATA+0x8>
  11. 起始运行地址0: r0 = 0x14 + 8 + 24 = 0x34 处的值 33f8002c 与实际运行地址0x0000002c不同 ,所以不正确
    1. 假设我要用这个地址去取值,或者用这个地址去跳转函数,而此时实际的变量或者函数根本不在链接地址处,这样肯定就出错了
  12. 起始运行地址0x3ff80000: r0 = 0x3ff80014 + 8 + 24 = 0x33f80034 处的值33f8002c和实际运行地址33f8002c相同 正确
    LDR-3
    1
    2
    ldr r0, =main/* 获取函数名的地址,位置有关 */
    ldr r0 ,=__bss_start /* 获取链接脚本里标号的地址,位置有关 */
  13. 这俩和 ldr r0, =SMRDATA 一致,位置有关,在0地址处运行不正确。

C函数

1.全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int a;
void abc(){ //c语言函数abc()
a = 2;
}

//对c语言函数反汇编

33f80040 <abc>:
33f80040: e52db004 push {fp} ; (str fp, [sp, #-4]!)
33f80044: e28db000 add fp, sp, #0
33f80048: e59f3010 ldr r3, [pc, #16] ; 33f80060 <abc+0x20> //取出全局变量a
33f8004c: e3a02002 mov r2, #2
33f80050: e5832000 str r2, [r3] //对全局变量赋值2
33f80054: e28bd000 add sp, fp, #0
33f80058: e8bd0800 pop {fp}
33f8005c: e12fff1e bx lr
33f80060: 33f800a0 mvnscc r0, #160 ; 0xa0



33f800a0 <a>: //全局变量a
33f800a0: 00000000 andeq r0, r0, r0
  1. R3 为全局变量 a 的地址,无论a 是存放在 0起始的地址还是0x33f80000起始的地址,它都认为 a 的地址是 0x33f800a0 。因此,C函数中调用全局变量是位置有关码。
2.函数调用
1
33f80084: ebffffed bl  33f80040 <abc>
  1. 由于 main 函数和 abc 函数挨得比较近,在32M范围之内,因此被翻译成了一条 bl 指令那么与位置无关。
  2. 如果,调用的函数比较远,大于32M的话,我认为是与位置有关系的,这个不再验证了。
3.局部变量

局部变量在函数刚开始的地方被压入栈,赋值语句被翻译成:

1
2
33f8007c: e3a03001 movr3, #1
33f80080: e50b3008 str r3, [fp, #-8]

2020年5月4日 下午12:16

  1. 我们来总结一下共享内存的创建和映射过程。
    1. 调用 shmget 创建共享内存。
      1. 先通过 ipc_findkey 在基数树中查找 key 对应的共享内存对象 shmid_kernel 是否已经被创建过,如果已经被创建,就会被查询出来,例如 producer 创建过,在 consumer 中就会查询出来。
      2. 如果共享内存没有被创建过,则调用 shm_ops 的 newseg 方法,创建一个共享内存对象 shmid_kernel。例如,在 producer 中就会新建。
    2. 在 shmem 文件系统里面创建一个文件,共享内存对象 shmid_kernel 指向这个文件,这个文件用 struct file 表示,我们姑且称它为 file1。
      1. 调用 shmat,将共享内存映射到虚拟地址空间。
      2. shm_obtain_object_check 先从基数树里面找到 shmid_kernel 对象。
      3. 创建用于内存映射到文件的 file 和 shm_file_data,这里的 struct file 我们姑且称为 file2。
      4. 关联内存区域 vm_area_struct 和用于内存映射到文件的 file,也即 file2,调用 file2 的 mmap 函数。
      5. file2 的 mmap 函数 shm_mmap,会调用 file1 的 mmap 函数 shmem_mmap,设置 shm_file_data 和 vm_area_struct 的 vm_ops。
    3. 内存映射完毕之后,其实并没有真的分配物理内存,当访问内存的时候,会触发缺页异常 do_page_fault。
      1. vm_area_struct 的 vm_ops 的 shm_fault 会调用 shm_file_data 的 vm_ops 的 shmem_fault。
      2. 在 page cache 中找一个空闲页,或者创建一个空闲页。
  2. 上面流程涉及到内存管理子系统、文件子系统,其实还是比较复杂的,下面我截取了两位读者的总结,对我的理解起到了非常大的作用:

2020年5月4日 下午12:04

总体认识:

  1. 进程进程之间的通信有多种方式:
    1. 管道(两种)
    2. 消息队列
    3. 共享内存
    4. 这三种共享的方式我认为都是对linux内核模块的调用,从而完成的业务功能而已,除了业务逻辑以外,也没啥好说的,就像别人开发出业务接口你调用就行了。
  2. 在进程通信中涉及到信号的知识,我觉得信号的特殊性在于:linux内核中就对信号进行了设计和规定,并且也有相应的内核机制的保证,他并不完全由上层的业务逻辑所决定。
  1. 信号的发送与处理是一个复杂的过程,这里来总结一下。
    1. 假设我们有一个进程 A,main 函数里面调用系统调用进入内核。
    2. 按照系统调用的原理,会将用户态栈的信息保存在 pt_regs 里面,也即记住原来用户态是运行到了 line A 的地方。
    3. 在内核中执行系统调用读取数据。
    4. 当发现没有什么数据可读取的时候,只好进入睡眠状态,并且调用 schedule 让出 CPU,这是进程调度第一定律。
    5. 将进程状态设置为 TASK_INTERRUPTIBLE,可中断的睡眠状态,也即如果有信号来的话,是可以唤醒它的。
    6. 其他的进程或者 shell 发送一个信号,有四个函数可以调用 kill、tkill、tgkill、rt_sigqueueinfo。
    7. 四个发送信号的函数,在内核中最终都是调用 do_send_sig_info。
    8. do_send_sig_info 调用 send_signal 给进程 A 发送一个信号,其实就是找到进程 A 的 task_struct,或者加入信号集合,为不可靠信号,或者加入信号链表,为可靠信号。
    9. do_send_sig_info 调用 signal_wake_up 唤醒进程 A。
    10. 进程 A 重新进入运行状态 TASK_RUNNING,根据进程调度第一定律,一定会接着 schedule 运行。
    11. 进程 A 被唤醒后,检查是否有信号到来,如果没有,重新循环到一开始,尝试再次读取数据,如果还是没有数据,再次进入 TASK_INTERRUPTIBLE,即可中断的睡眠状态。
    12. 当发现有信号到来的时候,就返回当前正在执行的系统调用,并返回一个错误表示系统调用被中断了。
    13. 系统调用返回的时候,会调用 exit_to_usermode_loop。这是一个处理信号的时机。
    14. 调用 do_signal 开始处理信号。
    15. 根据信号,得到信号处理函数 sa_handler,然后修改 pt_regs 中的用户态栈的信息,让 pt_regs 指向 sa_handler。同时修改用户态的栈,插入一个栈帧 sa_restorer,里面保存了原来的指向 line A 的 pt_regs,并且设置让 sa_handler 运行完毕后,跳到 sa_restorer 运行。
    16. 返回用户态,由于 pt_regs 已经设置为 sa_handler,则返回用户态执行 sa_handler。
    17. sa_handler 执行完毕后,信号处理函数就执行完了,接着根据第 15 步对于用户态栈帧的修改,会跳到 sa_restorer 运行。
    18. sa_restorer 会调用系统调用 rt_sigreturn 再次进入内核。
    19. 在内核中,rt_sigreturn 恢复原来的 pt_regs,重新指向 line A。
    20. 从 rt_sigreturn 返回用户态,还是调用 exit_to_usermode_loop。
    21. 这次因为 pt_regs 已经指向 line A 了,于是就到了进程 A 中,接着系统调用之后运行,当然这个系统调用返回的是它被中断了,没有执行完的错误。
  2. 我认为最关键理解的是第六步骤:其他的进程或者 shell 发送一个信号,有四个函数可以调用 kill、tkill、tgkill、rt_sigqueueinfo。
    1. 要理解这就话需要理解进程的相关知识,尤其是进程的调度 linux内核任务调度子系统

2020年5月4日 上午10:05

  1. 任务调度子系统可以从一下三个方面进行总结:
    1. 主要参考资料:刘超老师的趣谈操作系统
    2. 调度要解决的第一个问题是,每一个 CPU 小伙伴每过一段时间,都要想一下,白板上这么多项目,我应该干哪一个?CPU 的队列里面有这么多的进程或者线程,应该取出哪一个来执行?
      1. 有时候,进程会分优先级,如何给优先级高的进程多分时间呢?
        1. 这个简单,就相当于 N 个口袋,优先级高的袋子大,优先级低的袋子小。这样球就不能按照个数分配了,要按照比例来,大口袋的放了一半和小口袋放了一半,里面的球数目虽然差很多,也认为是公平的。
        2. vruntime:
          1. 函数 update_curr 用于更新进程运行的统计量 vruntime ,CFS 还需要一个数据结构来对 vruntime 进行排序,找出最小的那个。在这里使用的是红黑树。红黑树的节点是 sched_entity,里面包含 vruntime。
        3. 调度算法的本质就是解决下一个进程应该轮到谁运行的问题,这个逻辑在 fair_sched_class.pick_next_task 中完成。
    3. 调度要解决的第二个问题是,什么时候切换任务?也即,什么时候,CPU 小伙伴应该停下一个进程,换另一个进程运行?
      1. 方式一:主动调度
        1. A 项目做着做着,里面有一条指令 sleep,也就是要休息一下,或者等待某个 I/O 事件。那没办法了,要主动让出 CPU,然后可以开始做 B 项目。主动让出 CPU 的进程,会主动调用 schedule() 函数。
        2. 在 schedule() 函数中,会通过 fair_sched_class.pick_next_task,在红黑树形成的队列上取出下一个进程,然后调用 context_switch 进行进程上下文切换。
        3. 进程上下文切换主要干两件事情,一是切换进程空间,也即进程的内存,也即 CPU 小伙伴不能 A 项目的会议室里面干活了,要跑到 B 项目的会议室去。二是切换寄存器和 CPU 上下文,也即 CPU 将当期在 A 项目中干到哪里了,记录下来,方便以后接着干。
      2. 方式二:被动抢占
        1. 细分情形一:
          1. A 项目做着做着,旷日持久,实在受不了了。项目经理介入了,说这个项目 A 先停停,B 项目也要做一下,要不然 B 项目该投诉了。最常见的现象就是,A 进程执行时间太长了,是时候切换到 B 进程了。这个时候叫作 A 进程被被动抢占。
          2. cpu时钟Tick的中断处理函数:
            1. 抢占还要通过 CPU 的时钟 Tick,来衡量进程的运行时间。时钟 Tick 一下,是很好查看是否需要抢占的时间点。 时钟中断处理函数会调用 scheduler_tick(),他会调用 fair_sched_class 的 task_tick_fair,在这里面会调用 update_curr 更新运行时间。当发现当前进程应该被抢占,不能直接把他踢下来,而是把他标记为应该被抢占,打上一个标签 TIF_NEED_RESCHED。
        2. 细分情景二:
          1. 当一个进程被唤醒的时候。一个进程在等待一个 I_O 的时候,会主动放弃 CPU。但是,当 I_O 到来的时候,进程往往会被唤醒。这个时候是一个时机。当被唤醒的进程优先级高于 CPU 上的当前进程,就会触发抢占。如果应该发生抢占,也不是直接踢走当然进程,而也是将当前进程标记为应该被抢占,打上一个标签 TIF_NEED_RESCHED。
      3. 上面只是简单地给给进程打上了可以进行主动调度、被动调度的标签,就像是C语言中声明和初始化的区别一样
        1. 真正的抢占还是需要上下文切换,也就是需要那么一个时刻,让正在运行中的进程有机会调用一下 schedule。调用 schedule 有以下四个时机。
          1. 用户态的进程
            1. 对于用户态的进程来讲,从系统调用中返回的那个时刻,是一个被抢占的时机。
            2. 对于用户态的进程来讲,从中断中返回的那个时刻,也是一个被抢占的时机。
          2. 内核态的执行中
            1. 对内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,总是先调用 preempt_disable() 关闭抢占。再次打开的时候,就是一次内核态代码被抢占的机会。
            2. 在内核态也会遇到中断的情况,当中断返回的时候,返回的仍然是内核态。这个时候也是一个执行抢占的时机。
  2. 上面我们了解了调度系统的主要功能,接下来说说一个灵魂拷问:调度子系统和多进程/线程之间的关系?
    1. 首先,为什么我会问这样一个问题?因为我潜意识里认为调度子系统是多线程的管理者,即是一个master-slaver的关系,这可能和中国人的思维模式有关系。那么,我就会思考这样一个问题:既然调度子系统是master,那这个master由谁来管呢?这个问题就瞬间把我难倒了!
    2. 于是我开始重新思考他们两者之间的关系。结论:调度子系统和多线程/多进程本身就是一个东西,他么两个不是依赖管理的关系,本身他们就是一个东西,共生。
      1. 一个错误的解释:如果没有多线程,没有东西供他调度;没有调度系统,多进程无法正常进行。为什么说这句话是错的:还是那句话,他们两个本身就是一个东西!!
    3. 对于这个问题,我觉得从单cpu的角度比较很好理解:
      1. 对于单个任务来说,cpu按这个任务中包含的指令一条条的执行就行了
      2. 对于多任务来说,可以有两种方式:
        1. 一种是串行:这就变成了单任务的执行
        2. 一种是并发:
          1. 多个任务ABCD之间,他们在一个cpu上进行执行,会造成各自的执行过程其实是停停走走,但由于他们公用一套cpu寄存器,那么就会涉及到多次的状态保存,这对于ABCD来说是一样的。
          2. 那么,这时候就要小心一个问题:是谁帮他们保存的,我们知道cpu就是一个傻子,他是不会自己在干什么,在他眼里没有ABCD,只有一条条无差别的指令,更不会主动的去帮助你去完成数据的保存工作,以及恢复这两个工作。也就是说这两个工作也得写成一条条的程序指令。
          3. 所以从这个角度来说,所谓的调度子系统其实就是对程序员来说屏蔽了自己编写数据保存、恢复的工作,这样会给程序员造成错误的感觉:认为cpu好智能的,他会自动的保存和恢复,以及调度ABCD的运行。
          4. 调度子系统会将上层程序员编写的任务进行重新的拆解、分析,并生成cpu这个傻子需要的一条条的指令。这中间最最最关键的一点是:调度子系统也是一个task,调度这个词虽然是一个动词,但是调度子系统并不能完成调度的工作,他只是知道如何进行调度,时候一个理论派,不是实践派,他可以总结为一条条的步骤,其实就是指令。最终的调度这个动作,还是由cpu来完成的。所以,从cpu的角度来说,也不会区分ABCD和调度子系统,cpu认为他们是一样的。
          5. 调度子系统是一个线程_进程_task,只不过他完成的工作比较特殊,首先他在linux内核中动作,不是程序员自己写的,上层程序员不容易察觉;他本身的这个工种的特殊性:自己是进程_线程_task,他处理的对象也是进程_线程_task,对他们进行安排,一般来说我们都是处理数据、计算啥的,这就会造成一种自己处理自己的感觉,这点估计就是理解调度系统和多线程/进程的关键,也是最让人不舒服的地方,容易让人忽视他的作用,或者神化他的作用。
          6. 这中间理解的关键就是要从cpu无差别的角度去分析这个问题!

2020年5月4日 下午5:33
注:本文主要参考《C++ Primer》

总结:

  1. 我认为要区分这三个定义变量、函数、甚至是类中的使用方法,关键还是要站在需求的角度,千万别背,背会了也不会用。C++的设计者当初也是为了满足用户的需求在后期加上的这些功能,并且通过关键字的方式开放给程序员。

讲解static const的文章

c - Const vs Static Const - Stack Overflow
c - What is the difference between static const and const? - Stack Overflow

  • Addendum: Note that the behavior is different in C++. In C++, a const variable has internal linkage by default, so static const is redundant and extern const is necessary to get a constant with external linkage.

六个关键性的思考角度、概念:

  1. 名字作用域,对象声明周期 -> P184
  2. 编译,运行 -> P58
  3. 声明,定义 -> P41
  4. 有一个问题,为什么C++要设计六个概念?或者扩大点说,语言为什么要设计这些概念?
    1. 名字作用域,对象声明周期:域、周期都是表示的是范围的意思,在语言中加入范围的感念,可以有效的区分我们不再需要的定义、值,就像是标签一样,我们就可以将抛弃掉,有效的节省内存的使用
    2. 编译,运行 :关键是这里的编译,这其中就需要涉及到类型这个概念,动态类型语言不需要编译,静态类型需要需要编译(因为编译才定义成了静态类型语言),语言有了类型可以很好的帮助人类进行抽象,也可以进行进行类型的检查,将问题提前锁定在编译器更加安全
    3. 声明,定义:是C++为了运行程序拆分成多个逻辑部分来编写,所以支持了分离式编译,该机制允许将程序分割为若干的文件,每个文件可被单独编译。因此,C++将声明定义分开,这样就可以做到文件间共享代码的方法
      1. 声明:使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明
      2. 定义:负责创建于名字关联的实体

Static:全局变量 函数中 类中

  1. 全局变量:生命周期知道程序结束,作用域仅限于当前源文件
  2. 函数:P184局部对象->P43名字作用域,对象生命周期
    1. 自动对象->P40 默认初始化
    2. 局部静态对象
  3. 类:P269 类的静态成员
    1. 声明静态成员
    2. 使用类的静态成员
    3. 定义静态成员
    4. 静态成员的类内初始化
    5. 静态成员能用于某些场景,而普通成员不行

Constexpr:

  1. P58 constexpr常量表达式
    1. 常量表达式
    2. constexpr变量
    3. 字面值类型
    4. 指针和constexpr
  2. 函数中:P214
    1. constexpr函数
    2. 把内联函数和constexpr函数放在头文件内
  3. 类中:P267 字面值常量类
    1. constexpr构造函数

Const :

  1. static和const关键字的作用_JavaScript_许政的博客-CSDN博客
    1. 欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了
    2. 对指针来说
      1. 可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const
    3. 在一个函数声明中
      1. const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值
    4. 对于类的成员函数
      1. 若指定其为const类型,则表明其是一个常函数,不能修改类的 成员变量
    5. 对于类的成员函数
      1. 有时候必须指定其返回值为const类型,以使得其返回值不为“左值”
        1
        2
        3
        const classA operator*(const classA& a1,const classA& a2); //operator*的返回结果必须是一个const对象
        classA a, b, c;//如果不是,这样的变态代码也不会编译出错
        (a * b) = c; // 对a*b的结果赋值,显然不符合编程者的初衷,也没有任何意义
  2. 与constexpr的区别:
    1. constexpr的语义是:值在编译期就确定,并且在运行期不会改变。
    2. const指的是运行期:
      1. const变量的值可以在运行期再确定,编译器可以不确定,const保证的是运行期的不能改变变量的值,否则会报错
    3. 总结,constexpr和const最大的区别:值最晚什么时候确定,给的下限;并没有规定上限。
      1. const: |——————编译期———————|—————运行期———————|
      2. constexpr: |——————编译期———————|
      3. 上面表示的是值必须在指定区间中确定,否则出错
    4. eg:P58
      1. 使用const的常量表达式
        1
        2
        3
        4
        5
        6
        7
        8
        9
        // 常量表达式
        const int max_files = 20;//max_files是常量表达式
        const int limit = max_files + 1;//limit是常量表达式
        // 非常量表达式
        int staff_size = 27;//staff不是常量表达式
        const int sz = get_size();//sz不是常量表达式
        /*
        1.尽管staff_size的初始值是一个字面常量,但是由于他的数据类型只是普通的int而非const int,所以他不属于常量表达式
        2.尽管sz本身是一个常量,但是他的具体值知道运行时才能或得到,所以也不是常量表达式
      2. 使用constexpr的常量表达式
        1
        2
        3
        4
        // constexpr类型以便编译器来验证变量的值是否是一个常量表达式
        constexpr int mf = 20;//20是常量表达式
        constexpr int limit = mf + 1;//mf + 1是常量表达式
        constexpr int sz = size();//只有size是一个constexpr函数时,才是一条正确的声明语句

2020年5月3日 下午11:49

摘要:

  1. 为什么说进程和内存是linux内核最为关键的部件:因为linux内核中其他的子文件系统功能的完成依托于内存和进程的管理,在linux内核中,进程管理和内存管理子系统处在更底层的位置,linux内核中上层的子系统调用,从而完成相应的功能
    1. 当然,我这样说其实有些绝对,linux内核中的各个模块之间的关系并不是用上下级调用就可以解释清楚的,但是这样角度的整理,的确对理解linux内核有一定的帮助。
  2. 那么,这篇文章的目标有两个:
    1. 介绍一个进程和内存管理模块
    2. 说说其他子系统是如何使用这两个基础模块的

正文:

  1. 进程管理子系统核心功能介绍:
    1. 进程、线程的创建: 计算机是如何执行任务的(5个发展阶段)
    2. 进程(线程)的调度: linux内核任务调度子系统
  2. 内存管理子系统核心功能介绍:
    1. 将操作系统分解,看看这个庞大的系统中包含哪些子系统
  3. 关于调用进程、内存子系统的例子我这里用IPC中的共享内存方式举例: IPC中:共享内存的创建和映射过程

2020年5月2日 下午10:24

注:这篇文章题目很大,但我主要的目的是为了在学习linux系统的过程中串联一下知识,尤其是linux进程和内存相关的知识。从题目本身说更应该从计算机组成原理的角度去整理,如果从这个角度出发,那么推荐阅读:

  1. 性能之殇(一)— 天才冯·诺依曼与冯·诺依曼瓶颈 - 岁寒
  2. 极客时间的计算机组成原理课程

前提知识:

第一个阶段:只有cpu,没有内存

  1. 穿孔纸带

第二个阶段:cpu + 内存

  1. 有了内存有什么好处?

第三个阶段:cpu + 分段内存

  1. 下面分出这么多点,关键有两点:
    1. 函数调用的过程:栈帧是如何一步步的走的?函数调用过程的理解是理解线程调度的基础。
    2. linux的虚拟内存设计:要理解这么设计的原因,不要死背
  2. 为什么分段,有什么好处
    1. 进程运行过程中,代码指令根据流程依次执行,只需访问一次(当然跳转和递归可能使代码执行多次);而数据(数据段和BSS段)通常需要访问多次,因此单独开辟空间以方便访问和节约空间。具体解释如下:
    2. 防止程序指令被有意或无意地改写:
      1. 当程序被装载后,数据和指令分别映射到两个虚存区域。数据区对于进程而言可读写,而指令区对于进程只读。两区的权限可分别设置为可读写和只读。以防止程序指令被有意或无意地改写。
    3. 提高CPU缓存命中率:
      1. 现代CPU具有极为强大的缓存(Cache)体系,程序必须尽量提高缓存命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU一般数据缓存和指令缓存分离,故程序的指令和数据分开存放有利于提高CPU缓存命中率。
    4. 方便共享:
      1. 当系统中运行多个该程序的副本时,其指令相同,故内存中只须保存一份该程序的指令部分。若系统中运行数百进程,通过共享指令将节省大量空间(尤其对于有动态链接的系统)。其他只读数据如程序里的图标、图片、文本等资源也可共享。而每个副本进程的数据区域不同,它们是进程私有的。
    5. 可以区分变量的声明周期:
      1. 此外,临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据可能在整个程序执行过程中都需要访问,因此单独存储管理。堆区由用户自由分配,以便管理。
  3. linux的虚拟地址空间布局:
    1. Linux虚拟地址空间布局 - clover_toeic - 博客园
  4. 从存储的角度去理解函数调用栈:
    1. C语言函数调用栈(一) - clover_toeic - 博客园
    2. 函数调用过程中最关键的三个寄存器指针是:
      1. IP(Instruction Point) :指向处理器下一条等待执行的指令地址
      2. BP(Base Point) :栈帧基地址指针寄存器,存放执行函数对应栈帧的栈底地址,用于c运行库访问栈中的局部变量和参数。
      3. SP(Stack Point):堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址,且始终指向栈顶
    3. 为什么说这三个寄存器指针是关键呢?
      1. cpu其实就是一个傻子,你给他下达一个指令他动一下,他没有积极主动向性,主动的去找活干。对待这样的人你可倒了霉了,你做好饭叫他吃饭他都不会,必须从张嘴开始给他下达命令,这个命令就是桐楠格IP给他下达的。
      2. 关于BP和SP可以分为两个场景:
        1. 当前函数运行的时候,我们需要将局部变量保存在栈中,这是linux内存模型规定的,这个过程中我们需要使用BP、SP来保存当前函数使用栈的哪段位置区间,分为头和尾。
        2. 在函数调用的过程中,我们需要将调用函数的BP、SP保存起来在被调用函数的栈中。为什么不保存在堆中?因为没有必要,我们知道函数调用的过程是短暂的,并且函数调用可能会非常多,每次调用完之后,如果不释放的话就全是垃圾,而是用栈就能很好的做到自动释放。
      3. 函数调用的过程关键是上下文信息的保存,这里的上下文包括当前函数执行到了那里IP,使用了哪些局部变量local variable,栈帧BP/SP。当我们进行函数调用,切换到另外一个函数的时候,由于cpu寄存器是只有唯一的一份,所有的函数都使用这一个,那么必然会引起冲突,我们就需要将冲突的寄存器数据保存起来
  5. 如何实际的函数调用过程中栈使用情况:
    1. 反汇编
    2. gdb单步调试
    3. 直接运行程序print出来
    4. 缓冲区溢出详解 - clover_toeic - 博客园
  6. 函数调用栈的人为干扰(造成溢出是其中一种)方法原理:
    1. 缓冲区的定义:
      1. 缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C_C++语言中,通常使用字符数组和malloc_new之类内存分配函数实现缓冲区。
    2. 溢出的定义:
      1. 溢出指数据被添加到分配给该缓冲区的内存块之外。缓冲区溢出是最常见的程序缺陷。
    3. 从栈帧的角度解释原理:
      1. 栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来隐患。若将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统安全的目的。此外,堆栈的正确恢复依赖于压栈的EBP值的正确性,但EBP域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险。
    4. 语言层面的原因:
      1. 由于C/C++语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就会发生缓冲区溢出。攻击者可利用缓冲区溢出来窜改进程运行时栈,从而改变程序正常流向,轻则导致程序崩溃,重则系统特权被窃取。
    5. 缓冲区溢出详解 - clover_toeic - 博客园

第四个阶段:cpu + 分段内存 + 进程调度

  1. 进程的调度与函数之间的调用有什么区别?
    1. 从执行阶段进行比较:
      1. 一个进程中包含很多的函数调用来完成一个进程的任务,也就是函数调用是进程的子集。从这个角度来说,进程和函数是不应该进行比较的,他们是两中不同体量的任务。
    2. 调用和调度的区别:
      1. 函数调用的过程中,比如A调用B,这个调用是在A函数中在代码中就指明确定的;而进程的调度,是在进程发生主动调度、被动调度之后,由linux内核中的调度类进行选择下一个需要执行的进程。注意进程中多的这个调度类,我认为这就是函数调用和进程调度之间的区别:一个是调用,一个是调度,从文字上也能很好的解释。
    3. 可以进行调用、调度的背后机制:
      1. 多进程任务的完成依靠的是调度类进行管理,而函数调用任务的完成依靠的是内存的栈帧结构的支持。栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持

第五个阶段:cpu + 分段内存 + 进程调度 + 线程

  1. 有了进程的调度,我们就已经可以完成进程的并发执行、中断、进程的状态的转换等等复杂的功能,为什么我们还需要设计一个线程呢?
    1. 这里就需要提到老生常谈一句话:进程是linux资源的分配单位,线程是cpu调度和执行的基本单位。
    2. 上面提到的这个概念其实就和范式编程中提到的logic、control、data的分离,思想上是一致的。专门的这样分离式的设计更加符合我们人类的抽象思维。通过这样的设计我们能够让上层程序员强调资源和执行的概念区别,也提示开发人员在进行开发的时候,可以从这两个角度进行思考
    3. 同时,也类似于分段内存的设计一样,这样的设计也不仅仅有利于人类思维,对于当我们多个任务需要相同的资源的时候,这种底层的支持就显得至关重要,自然的降低了重复资源的拷贝,让操作系统也容易的整理自己的资源。
  2. 解释了为什么我们需要线程,那么在linux内核中线程是如何在进程的基础上实现的呢?
    1. 先说一下我的结论:线程的创建过程是进程这个任务执行出来的!一般来说,进程对程序员来说是完成它规定的业务功能的,而我结论的意思是:进程这个task生成了另一个task,也可以说这是task生成task是一个特殊的业务功能。
      1. 这其中有一点需要强调一下,这里所谓的进程task其实也是线程task,因为创建一个进程之后,这个进行本身就是一个线程,或者说这个进程中本身就包含一个线程来执行任务
      2. 从linux内核的角度看来,线程和进程都会变成task
    2. 说完结论,我说为什么我会有这样的认识:
      1. 首先,我们需要知道在linux内核中给进程创建新的线程,这个过程中最最重要的就是线程栈,因为线程栈是线程执行过程底层实现核心机制。在进程的创建过程中,注意这里是进程,不是线程,我们根据linux虚拟内存模型的知识可以知道其中有栈内存段来控制进程(唯一的线程)的执行过程,那么新来的线程他的线程栈如何分配呢?栈这个东西应该是不可以多个线程公用的,所以一个合理的位置就是内存模型中的堆段,堆段在linux2.6中有将近2.9G的空间,而栈区默认只有8M。
      2. 另外,老师曾经说过:完成线程的创建是用户态和内核态共同完成的。我认为的结论正好可以解释这样的行为,进程的在执行的过程中就需要用户态和内核态共同完成功能,只不过完成的功能是创建另个task而已。
  3. 进程,线程与多核,多cpu之间的关系 - 南宫饱虎 - 博客园
  4. 内核栈和用户栈_运维_qq_41727218的博客-CSDN博客

#b计算机基础/c_计算机系统/b_linux系统/补充

2020年5月2日 下午8:48

简单的讲解:

深入的讲解

IO模式和IO多路复用 - ZingpLiu - 博客园

  1. blocking和non-blocking的区别
    1. 调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
  2. synchronous IO和asynchronous IO的区别
    1. 在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
        • A synchronous I_O operation causes the requesting process to be blocked until that I_O operation completes;
        • An asynchronous I/O operation does not cause the requesting process to be blocked;
      1. 两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
    2. 有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
    3. 而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
  3. non-blocking IO和asynchronous IO的区别
    1. 可以发现non-blocking IO和asynchronous IO的区别还是很明显的。
    2. —在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存
    3. 而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。