0%

声明

此文章是搬运来的,原地址点击此处,写的真好,能看的非常懂,原作者有个地方可能笔误写错了,做了改正。

输入IO

这里所说的输入IO,指的是只作为输入,不具有输出功能。此时对于input引脚的要求就是高阻(高阻与三态是同一个概念)。基本输入电路的类型大致可以分为3类:基本输入IO电路、施密特触发输入电路以及弱上拉输入电路。

先从最基本的基本输入IO电路说起,其电路如图 1所示。

220426-GPIO输入输出各种模式-1.jpg

其中的缓冲器U1是具有控制输入端,且具有高阻抗特性的三态缓冲器。通俗地说就是这个缓冲器对外来说是高阻的,相当于在控制输入端不使能的情况下,物理引脚与内部总线之间是完全隔离的,完全不会影响内部电路。而控制输入端的作用就是可以发出读Pin状态的操作指令。其过程如图 2所示。

220426-GPIO输入输出各种模式-2.jpg

这种基本电路的一个缺点是在读取外部信号的跳变沿时会出现抖动,如下图所示。

220426-GPIO输入输出各种模式-3.jpg

于是施密特触发输入电路就是解决了上述这种抖动的问题,其经过施密特触发器后的信号如图 4所示。

220426-GPIO输入输出各种模式-4.jpg

对于输入电路还存在另外一个问题,就是当输入引脚悬空的时候,输入端检测到的电平是高还是低?当输入信号没有被驱动,即悬空(Floating)时,输入引脚上任何的噪声都会改变输入端检测到的电平,如图 5所示。

220426-GPIO输入输出各种模式-5.jpg

为了解决这个问题,可以在输入引脚处加一个弱上拉电阻,如图 6所示。

220426-GPIO输入输出各种模式-6.jpg

这样,当输入引脚悬空时,会被RP上拉到高电平,在内部总线上就有确定的状态了。

但是这种结构是有一定问题的。首先很明显的一点是,当输入引脚悬空时读到的是1,当输入引脚被高电平驱动时读到的也是1,只有当输入引脚被低电平驱动时读到的才是0。也就是对于读1采取的方式是”读取非零”的方式。

另一个问题是该电路对外呈现的不是高阻,某种意义上说也在向外输出,当外部驱动电路不同时可能出现错误的检测结果。例如外部驱动电路是如图 7所示的结构,该电路结构中通过K打到不同端可以输出高电平或者低电平。

220426-GPIO输入输出各种模式-7.jpg

如果将如图 7所示的电路输出低电平,连接到带有弱上拉电阻的输入引脚,其结构如下所示。

220426-GPIO输入输出各种模式-8.jpg

由欧姆定律知,测试点处的电平是4.545V,于是CPU测得的输入信号为高,而外部驱动电路希望输出的电平为低。这种错误的原因就在于这种结构的输入电路并不是真正的高阻,或者说这个输入IO其实也在输出,而且影响了外部输入电路。

这种情况的发生也说明了:信号前后两级传递,为什么需要输出阻抗小,输入阻抗大的原因。在这个例子中,外围驱动电路的输出阻抗很大,达到了100Kohm;而输入端的阻抗又不够大,只有10Kohm,于是就出现了问题。如果输入端的输入阻抗真正做到高阻(无穷大),如下所示,就不会出现问题。

220426-GPIO输入输出各种模式-9.jpg

上面提到的这个带弱上拉的输入电路,也就是在后续章节会提到的准双向端口的情况。

输出IO

IO输出电路最主要的两种模式分别是推挽输出(Push-Pull Output)和开漏输出(Open Drain Output)。

推挽输出(Push-Pull Output)

推挽输出的结构是由两个三极管或者MOS管受到互补信号的控制,两个管子始终保持一个处于截止,另一个处于导通的状态。如图 10所示。

220426-GPIO输入输出各种模式-10.jpg

推挽输出的最大特点是可以真正能真正的输出高电平和低电平,在两种电平下都具有驱动能力。

补充说明:所谓的驱动能力,就是指输出电流的能力。对于驱动大负载(即负载内阻越小,负载越大)时,例如IO输出为5V,驱动的负载内阻为10ohm,于是根据欧姆定律可以正常情况下负载上的电流为0.5A(推算出功率为2.5W)。显然一般的IO不可能有这么大的驱动能力,也就是没有办法输出这么大的电流。于是造成的结果就是输出电压会被拉下来,达不到标称的5V。

当然如果只是数字信号的传递,下一级的输入阻抗理论上最好是高阻,也就是只需要传电压,基本没有电流,也就没有功率,于是就不需要很大的驱动能力。

对于推挽输出,输出高、低电平时电流的流向如图 11所示。所以相比于后面介绍的开漏输出,输出高电平时的驱动能力强很多。

220426-GPIO输入输出各种模式-11.jpg

但推挽输出的一个缺点是,如果当两个推挽输出结构相连在一起,一个输出高电平,即上面的MOS导通,下面的MOS闭合时;同时另一个输出低电平,即上面的MOS闭合,下面的MOS导通时。电流会从第一个引脚的VCC通过上端MOS再经过第二个引脚的下端MOS直接流向GND。整个通路上电阻很小,会发生短路,进而可能造成端口的损害。这也是为什么推挽输出不能实现” 线与”的原因。

开漏输出(Open Drain Output)

常说的与推挽输出相对的就是开漏输出,对于开漏输出和推挽输出的区别最普遍的说法就是开漏输出无法真正输出高电平,即高电平时没有驱动能力,需要借助外部上拉电阻完成对外驱动。下面就从内部结构和原理上说明为什么开漏输出输出高电平时没有驱动能力,以及进一步比较与推挽输出的区别。

首先需要介绍一些开漏输出和开集输出。这两种输出的原理和特性基本是类似的,区别在于一个是使用MOS管,其中的”漏”指的就是MOS管的漏极;另一个使用三极管,其中的”集”指的就是MOS三极管的集电极。这两者其实都是和推挽输出相对应的输出模式,由于使用MOS管的情况较多,很多时候就用”开漏输出”这个词代替了开漏输出和开集输出。

介绍就先从开集输出开始,其原理电路结如图 12所示。

220426-GPIO输入输出各种模式-12.jpg

图 12左边的电路是开集(OC)输出最基本的电路,当输入为高电平时,NPN三极管导通,Output被拉到GND,输出为低电平;当输入为低电平时,NPN三极管闭合,Output相当于开路(输出高阻)。高电平时输出高阻(高阻、三态以及floating说的都是一个意思),此时对外没有任何的驱动能力。这就是开漏和开集输出最大的特点,如何利用该特点完成各种功能稍后介绍。这个电路虽然完成了开集输出的功能,但是会出现input为高,输出为低;input为低,输出为高的情况。

图 12右边的电路中多使用了一个三极管完成了”反相”。当输入为高电平时,第一个三极管导通,此时第二个三极管的输入端会被拉到GND,于是第二个三极管闭合,输出高阻;当输入为低电平时,第一个三极管闭合,此时第二个三极管的输入端会被上拉电阻拉到高电平,于是第二个三极管导通,输出被拉到GND。这样,这个电路的输入与输出是同相的了。

接下来介绍开漏输出的电路,如图 13所示。原理与开集输出基本相同,只是将三极管换成了MOS而已。

220426-GPIO输入输出各种模式-13.jpg

接着说说开漏、开集输出的特点以及应用,由于两者相似,后文中若无特殊说明,则用开漏表示开漏和开集输出电路。

  1. 开漏输出最主要的特性就是高电平没有驱动能力,需要借助外部上拉电阻才能真正输出高电平,其电路如图 14所示。

    220426-GPIO输入输出各种模式-14.jpg

当MOS管断开时,开漏输出电路输出高电平,且连接着负载时,电流流向是从外部电源,流经上来电阻RPU,流进负载,最后进入GND。

  1. 开漏输出的这一特性一个明显的优势就是可以很方便的调节输出的电平,因为输出电平完全由上拉电阻连接的电源电平决定。所以在需要进行电平转换的地方,非常适合使用开漏输出。
  2. 开漏输出的这一特性另一个好处在于可以实现”线与”功能,所谓的”线与”指的是多个信号线直接连接在一起,只有当所有信号全部为高电平时,合在一起的总线为高电平;只要有任意一个或者多个信号为低电平,则总线为低电平。而推挽输出就不行,如果高电平和低电平连在一起,会出现电流倒灌,损坏器件。

推挽与开漏输出的区别

220426-GPIO输入输出各种模式-15.jpg

双向IO

很多处理器的引脚可以设置为双向端口,双向端口的要求就是既可以输出信号,又可以读回外部信号输入。要同时做到这两点从原理上来说有点困难,首先从处理器的开漏输出IO口的内部结构说起,如图 16所示。

220426-GPIO输入输出各种模式-16.jpg

该结构是在图 13的基础上,在三极管之前加入了一个FF,目的是用于控制输出信号的时间。比较常见的一个应用场合是多个IO作为一个总线时,需要总线上的各个引脚同时将数据输出。

对于开漏输出结构,会将FF的输出Q端连接会输入驱动缓冲器,这样的话执行读操作是读的并不是外部引脚的状态,而是自己输出的状态。

双向开漏IO

但是对图 16的结构稍作修改,如图 17所示时,该结构称为双向开漏IO的结构。所做的改动是将输入驱动缓冲器连接到了PIN上。

220426-GPIO输入输出各种模式-17.jpg

该结构输出为”1”时,T1断开,此时pin对外呈现高阻,作为输入引脚没有任何问题。但是如果该结构输出”0”时,T1导通,此时pin对外短路到地,即无论外部输入什么信号,U2读回的全部是低。所以对于这样的结构,如果需要作为输入引脚使用时,必须给U1输出”1”后才能读取外部引脚数据。

准双向开漏IO

很多文献中还提到了准双向端口,其实准双向端口就是图 17的结构中加了一个上拉电阻,如图 18所示。

220426-GPIO输入输出各种模式-18.jpg

这个结构与图 17相比有以下相同与不同之处:

  1. 作为输入引脚使用时,也必须先向U1中写”1”,以达到断开T1的目的。所以是否需要提前写”1”并不是双向IO与准双向IO的区别。两者做输入端口时都需要提前写”1”。
  2. 双向端口作为输入时是真正的高阻态,而准双向IO作为输入端口时,输入阻抗不为高阻,于是有可能出现如本文图 8所示的问题。
  3. 准双向端口读取输入状态,默认为高。也就是判断外部输入信号的方法是”非低则为高”。即该结构只能准确的识别外部的低电平,无法区分悬空和真正的高。于是只要读到的不是0,都认为外部为1。

推挽输出作为双向IO

如果双向端口中的输出部分采用的是推挽输出结构,那么作为输入时必须将上下两个管子全部端口才能成为高阻,作为输入。

51单片机的P0端口

在双向端口的讨论中,比较复杂的就是51单片机的P0端口了。这里就详细讨论一下51单片机的P0端口结构和工作原理。

P0端口的内部结构如图 19所示。

220426-GPIO输入输出各种模式-19.jpg

内部结构比较复杂,包括以下这些器件:

  1. U1:与门。一个输入连着控制线,另一个输入连接这地址/数据信号。由于与门的特性,当控制线为1时,与门输出与地址/数据信号的电平保持一致;如果控制线为0,则输出恒为。于是控制信号线相当于与门的使能信号。
  2. U2:反相器,输出信号为地址/数据信号的反相信号。
  3. U3和U6都是具有控制输入端且具有高阻抗特性的三态缓冲器,作用是对于外部呈现高阻态。当控制端使能时可以将外部信号的电平读进数据总线。
  4. U4:为锁存器,目的就是控制引脚输出信号的时间。
  5. U5:模拟开关,可以控制V2的输入信号是来自锁存器U4的Q非输出还是来自于反相器U2的输出。
  6. V1和V2分别是两个MOS管。

了解了各个独立器件之后就开始介绍工作在各个模式下的工作原理:

P0用于地址/数据线时:

在P0作为地址/数据线时,是地址、数据复用总线,P0需要输出地址,同时需要读回数据信号。

当P0需要输出地址信息时,U1的控制信号为0,模拟开关U5接到U2反相器的输出。于是当地址信号线传来的信号为1,与控制线”1”相与之后输出到V1的输入信号为”0”,V1截止。地址信号”1”经反相之后,通过模拟开关输出到V2的输入端为”1”,V2导通,于是情况如图 20所示,pin输出”0”。

220426-GPIO输入输出各种模式-20.jpg

当地址信号线传来的信号为1,与控制线”1”相与之后输出到V1的输入信号为”1”,V1导通。地址信号”0”经反相之后,通过模拟开关输出到V2的输入端为”0”,V2截止,于是情况如图 21所示,pin输出”1”。

220426-GPIO输入输出各种模式-21.jpg

于是在作为地址线输出时,V1、V2两个MOS管均使用了,是推挽输出。

当P0在输出低8位地址信息后,将变为数据总线,此时CPU的操作是控制端输出0,模拟开关打到锁存器的Q非端,且向锁存器中打入”1”。于是Q非输出为0,V2截止。同时控制线为0使得与门输出为0,V1截止。由于V1和V2都截止,所以此时pin对外完全呈现高阻,作为输入端口,外部数据通过U6进入内部总线,情况如图 22所示。(相当于将推挽输出的两个MOS管全部断开了)此时由于对外呈现高阻,所以是真正的输入引脚。这就解释了为什么说P0是真正的双线端口。

220426-GPIO输入输出各种模式-22.jpg

P0用于普通IO时:

在P0作为普通IO并作为输出时,控制信号为0,使V1始终处于截止状态。模拟开关连接到Q非输出,当作为输出时,锁存器的输入端直接输入0或者1,Q非将反相信号输入到V2的输入端。即当输出”0”时,V2输入端为”1”,V2导通,pin输出”0”;当输出”1”时,V2输入端为”0”,V2截止,pin输出高阻的0。即当P0工作在普通IO模式下,输出为开漏输出,且内部没有上拉电阻。

在P0作为普通IO并作为输入时,控制信号为0,使V1始终处于截止状态。模拟开关连接到Q非输出,且CPU自动向锁存器输入端写1,则V2输入端为0,V2截止。与之前在作为地址/数据线,作为输入时一样,也是两个MOS管全部断开,pin直接连接到U6,对外呈现高阻。于是也是真正的输入引脚。

综上P0无论工作在哪种模式下都是真正的双端口IO。

51单片机的P1~P3端口

51单片机的其他三个端口的内部结构如图 23所示,与P0相比简单了很多,没有了顶部的MOS管,也没有了地址/数据信号的选项。作为输出时是带有上拉电阻的的开漏输出,作为输入时是有上拉电阻存在的,于是输入端口对外不是高阻。这就解释了为什么P1~P3只能是准双向端口。

220426-GPIO输入输出各种模式-23.jpg

数组指针&指针数组

参考:https://www.cnblogs.com/mq0036/p/3382732.html,两者内存分布写的很详细。

数组指针

定义int (*p)[n];

()优先级比[]高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。

如要将二维数组赋给一指针,应这样赋值:

1
2
3
4
int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p = a; //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

所以数组指针也称指向一维数组的指针,亦称行指针。

指针数组

定义int *p[n];

[]优先级比*高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值。
如要将二维数组赋给一指针数组:

1
2
3
4
5
int *p[3];
int a[3][4];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i] = a[i];

这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2],所以要分别赋值。

常量指针&指针常量

指针常量

定义int * const p = &a;

p是个常量,p的类型是int*,所以p的值不能改变,但是*p的值可以改变。

常量指针

定义const int * p = NULL;

*p是个常量,p的类型是int*,所以*p的值不能改变,但是p的值可以改变。利用这个特性,在函数传参时使用常量指针可以防止函数对变量值的误修改。

常量指针常量

定义const int * const p = &a;

p是个常量,*p也是个常量,p的类型是int*,所以*p的值不能改变,p的值也不能改变。

引用&常量引用

引用

定义int& r = a;

C++ 引用,引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。

函数可以通过引用传参,void foo(int &a){a = 1;}修改引用形参可以改变实参。

常量引用

定义const int& r = a;

函数可以通过常量引用传参,void foo(const int &a) {;}只能读取a,不能修改a。利用这个特性,在函数传参时使用常量指针可以防止函数对变量值的误修改。

函数指针&指针函数

函数指针

定义函数指针void (*pFun)(int a, int b);

定义函数void Fun(int a, int b) {;}

将Fun函数的首地址赋给指针变量pFunpFun = Fun;也可以是pFun = &Fun;

通过指针函数调用pFun(1, 2);也可以是(*pFun)(1, 2);

函数指针定义时返回值,参数类型,个数都必须与要指向的函数相同。

指针函数

指针函数是返回类型为指针的函数。

C结构体内存对齐

课程链接

课程笔记

L1~L3

  1. 课程目标:进入操作系统,学习操作系统的运作。

  2. 计算机工作方式:取址执行,取CS和PC指针指向内存中的指令,在CPU执行

  3. 操作系统引导:启动引导时内核在内存中的位置和移动后的位置情况图220326-操作系统-1.jpg

    开机时,CS=0xffff,IP=0x0000,CPU处于实模式。实模式寻址CS<<4+IP,CS存放段地址,IP存放4位段偏移量,共20位。0xffff0地址属于BIOS映射区,在BIOS里检查硬件(RAM,键盘,…);

    将磁盘0磁道0扇区(引导扇区)读到内存0x7c00,引导扇区代码存放在bootsect.s;

    将引导扇区代码从内存0x7c00处移动到0x90000处;

    jmpi go, INITSEG,INITSEG=0x9000,跳至bios的go标号处;

    jmpi 0, SETUPSEG,SETUPSEG=0x9020,跳至setup.s;

    在setup.s start:中读取扩展内存大小,读取显卡参数,…;

    do_move:中将system模块移动到0地址,jmpi 0, 8,跳转到system模块,system模块开始是head.s;

    通过设置cr0最后一位,PE=1由16位实模式转到32位保护模式,在保护模式下,CS表示选择子,查GDT表得到;

    在head.s中初始化GDT表和IDT表,setup_paging执行ret后,会执行函数main(),进入main()后的栈为0,0,0,L6,三个0表示main函数的参数,L6表示main函数返回时进入死循环;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    after_page_tables:
    pushl $0 # These are the parameters to main :-)
    pushl $0 # 这些是调用 main 程序的参数(指 init/main.c)。
    pushl $0 # 其中的'$'符号表示这是一个立即操作数。
    pushl $L6 # return address for main, if it decides to.
    pushl $_main # '_main'是编译程序对 main 的内部表示方法。
    jmp setup_paging # 跳转至第 198 行。
    L6:
    jmp L6 # main should never return here, but
    # just in case, we know what happens.
    # main 程序绝对不应该返回到这里。不过为了以防万一,
    # 所以添加了该语句。这样我们就知道发生什么问题了。
    setup_paging:
    设置页表...
    ret

L4~L7

  1. 操作系统接口:接口表现为函数调用,又由系统提供,所以称为系统调用,可移植操作系统接口(Portable Operating System Interface of uniX, POSIX)
  2. 系统调用的实现:应用层要通过接口访问内核,不能直接读内核内存,内核是受保护的。中断是进入内核的唯一方法;220326-操作系统-2.jpg220326-操作系统-3.jpg
  3. 课程任务:操作系统要学习CPU管理、内存管理、文件管理;

L8~L19

  1. CPU管理:多道程序,交替执行(并发)。运行的程序是进程,多进程切换时要保存现场。通过进程控制块(Process Control Block, PCB)来记录进程信息。操作系统要把这些进程记录好,要按照合理的次序分配资源、调度;

  2. 进程状态图:220326-操作系统-4.jpg

  3. 通过进程内存映射表来实现内存分离,切换进程时要切换内存映射表;通过上锁,来保证共享内存的正确被读写;

  4. 用户级线程和核心级线程区别:用户级线程创建、切换无需进内核,一个线程阻塞进程会被切换;

  5. 线程的优点:线程切换无需切换内存映射表。220326-操作系统-5.jpg

  6. 用户级线程切换通过Yield(),要先切换栈,栈会存在TCB(线程控制块)中,每个线程对应一个TCB;

  7. 核心级线程切换时除了用户栈还有内核栈也要一起切换。在内核阻塞时要进行TCB切换,使用switch_to(cur, next);TCB切换完后根据TCB完成内核栈切换,最后IRET返回用户态,切换用户栈;

  8. 前台任务需要响应时间小->导致切换次数多->导致系统内耗大->导致吞吐量(完成任务的量)小。后台任务更加关注周转时间。任务可分成IO约束型任务和CPU约束型任务;

  9. CPU调度策略:FIFO、Priority;CPU调度算法:先来先服务(First come, first served),短作业优先(SJF)这个算法周转时间最小,按时间片来轮转调度(RR),优先级调度(前台>后台);

  10. linux0.11中的调度函数既考虑了优先级又考虑了时间片:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void Schedule(void) // 在 kernel/sched.c 中
    { while(1){ c=-1; next=0; i=NR_TASKS; p=&task[NR_TASKS];
    while(--i){ if((*p->state == TASK_RUNNING&&(*p)->counter>c)
    c=(*p)->counter, next=i; }
    if(c) break; // 找到了最大的counter,跳出,执行switch_to()
    for(p=&LAST_TASK;p>&FIRST_TASK;--p)
    (*p)->counter=((*p)->counter>>1)+(*p)->priority;
    }
    switch_to(next);}
  11. 进程间同步看信号量(Semaphore),可使多个进程合理有序运行。读写信号量的代码一定在临界区,或者原子操作。临界区:一次只允许一个进程进入该进程的那段代码。原子操作:不会被线程调度机制打断的操作;

  12. 保护临界区的方法:关闭中断cli();,临界区,开中断sti();,剩余区。这种方法只适用于小系统,不适用于多核CPU。还可以采取硬件原子指令法,硬件一条指令修改mutex变量;

  13. 信号量未互斥使用会造成死锁。死锁处理:死锁预防,死锁避免,死锁检测+恢复,死锁忽略;

L20~L25

  1. 内存使用:将程序放到内存中,PC指向开始的地址。将程序从硬盘载入内存需要重定位,物理地址=基址+逻辑地址。重定位可以在编译时(静态系统)、载入时执行、运行时执行。编译时重定位的程序只能放在内存固定位,载入时重定位的程序一旦载入内存就不能动了;220326-操作系统-6.jpg

  2. 程序载入后还需要移动,引入交换(swap)概念,可以把暂时不用的内存搬到交换分区,充分利用内存。

  3. 引入分段,程序由若干部分(段)组成,每个段有各自的特点、用途:主程序(只读)、变量集(可写、不会动态增长)、函数集、动态数组(会动态增长)、栈。使用分段思想可以让用户分治每个段,可以让内存更高效使用。定位具体指令mov [es:bx], ax。地址组成:<段号,段内偏移>。进程段表存放在LDT表,系统段表存放在GDT表,LDT存放在PCB中;220326-操作系统-7.jpg

  4. 内存分区可分为固定分区和可变分区。可变分区的管理:空闲分区表、已分配分区表。可变分区分配内存算法:首先适配(最快)、最佳适配和申请空间长度最接近(会导致内存碎片)、最差适配;

  5. 实际物理内存采用分页来管理,分区是对虚拟内存(交换分区)的管理方法。分区会造成内存碎片,分页会将物理内存分割(比如每4k分割),程序也会被分页,每段都会被分页,分页会使内存存储离散化。地址翻译有专门的硬件内存管理单元(MMU)执行;220326-操作系统-8.jpg

  6. 为了提高内存空间利用率,页应该小,但是页小了页表就大了。如果页表只存放用到的页,则需要顺序查找页表,速度慢。采用多级页表可兼顾速度和空间,即页目录表+页表,地址组成<10bits页目录号,10bits页号,12bits偏移>,12bits偏移刚好是4K。

  7. 多级页表增加了访存的次数,尤其是64位系统。硬件上引入转译后备缓冲区(Translation Look-aside Buffer,TLB,快表),快表能存放最近查过多级页表的逻辑页和物理页,可以根据页号直接查物理页号;220326-操作系统-9.jpg

  8. 段页结合:段面向用户,页面向硬件。实际的段页内存管理,程序如何载入内存,fork后内存做了什么可以看L23 段页结合的实际内存管理220326-操作系统-10.jpg

  9. 用内存换入换出实现“大内存”,虚拟内存4G,物理内存可以只有1G。当程序运行缺页时, page fault中断请求do_no_page调页(读磁盘),这就是换入;

  10. 内存换出:将内存中不用的页换出到磁盘。如何找不用的页?换出算法:FIFO(先来的先被换出,不太行)、MIN(将未来最远要使用的页淘汰,是最优方案,但无法预测未来)、LRU(Least Recently Used,最近最少使用);

  11. LUR的准确实现:可以用时间戳,每次地址访问都需要修改时间戳,需维护一个全局时钟,实现代价较较大,可以用页码栈,选择栈底淘汰,代价也太大。在实际中内存换出算法采用LUR的近似实现:时钟算法(环形队列)220326-操作系统-11.jpg220326-操作系统-12.jpg

  12. 给进程分配多少页框?分配多了,请求调页导致的内存高效利用就没有了。分配少了,会频繁请求调页导致系统低效。系统低效解释:系统内进程增多多,每个进程的缺页率增大大,缺页率增大到一定程度,进程总等待调页完成,CPU利用率降低低,进程进一步增多,缺页率更大,称这一现象为颠簸(thrashing);

L26~L32

  1. IO设备、显示器、键盘,让这些外设工作的基本思想就是往外设硬件寄存器写值,外设处理完再产生中断,CPU处理。在Linux中无论什么设备都用文件接口open、read、write、close,例如int fd = open("/dev/xxx");到最后是通过out写对应寄存器;

  2. 生磁盘的使用:磁盘驱动负责从盘块号(block)计算出柱面(cgl)、磁头(head)、扇区号(sec)。从CHS到扇区号,从扇区到盘块(第一层抽象):扇区号 = C×H×S+H×S+S220326-操作系统-13.jpg

  3. 多个进程通过请求队列使用磁盘(第二层抽象);220326-操作系统-14.jpg

  4. 访问磁盘时间 = 写入控制器时间 + 寻道时间(8~12ms)+ 旋转时间(7200rmp,半周4ms)+ 传输时间(50MB/s,约0.3ms)。可见访问磁盘主要时间花在寻道,block相邻的盘块可以快速读出,因此需要考虑寻道算法。寻道调度算法有FCFS(先来先服务)、SSTF(短寻道优先算法,可能有磁道长时间访问不到)、SCAN(SSTF+中途不折回)、C-SCAN(SCAN+直接移动到另一端,电梯算法);

  5. 引入文件(第三层抽象),文件是建立字符流在磁盘块集合的映射关系,每个文件都对应一个文件控制块(FCB),连续结构存储FCB包含文件名、起始块、块数的信息,链式结构存储FCB包含文件名、起始块的信息,链式结构存储顺序访问慢、可靠性差,索引结构存储文件是连续和链式的有效折中,FCB包含文件名、索引块的信息。在实际系统中采用的是多级索引结构,根据不同大小文件分成不同级索引通过inode一级级查询访问,因此可以标识很大的文件,很小的文件可以高效访问,中等大小的文件访问速度也不慢;

  6. 引入文件系统(第四层抽象),文件系统引入目录树,文件目录也是个文件,存放目录下其他文件名和对应文件的FCB指针。inode位图:哪些inode空闲,哪些被占用。盘块位图:哪些盘块是空闲的,硬盘大小不同这个位图的大小也不同。超级块:存放i节点位图和盘块位图的大小,因此可以计算出i节点起始地址;220326-操作系统-15.jpg220326-操作系统-16.jpg220326-操作系统-17.jpg

课程总结

通过本次操作系统的学习,了解了操作系统全图(CPU、内存、文件设备),学习了多进程视图、文件视图。哈工大的几个实验非常的难,Study OS by coding !!!

GDB介绍

GDB是什么

GDB: The GNU Project Debugger,GDB是GNU开源组织发布的一个强大的Linux下的程序调试工具,GDB主要帮助你完成下面四个方面的功能:

  • 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
  • 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
  • 当程序被停住时,可以检查此时你的程序中所发生的事。
  • 你可以改变你的程序,将一个BUG产生的影响修正从而测试其他BUG。

GDB支持哪些语言

GDB主要来调试C/C++语言写的程序,当然也就可以调试其他语言程序,另外的语言没了解过。

GDB使用

在Ubuntu中安装GDB

1
2
sudo apt-get update
sudo apt install gdb

补一下GCC的知识

GCC参数详解,gcc 与 g++ 分别是 gnu 的 c & c++ 编译器 gcc/g++ 在执行编译工作的时候,总共需要4步:

  • 预处理,生成 .i 的文件[预处理器cpp],指令:-E

  • 将预处理后的文件转换成汇编语言, 生成文件 .s [编译器egcs],指令:-S

  • 由汇编变为目标代码(机器代码)生成 .o 的文件[汇编器as],指令:-c

  • 连接目标代码, 生成可执行程序 [链接器ld],指令:-o FILE

在配合GDB时,gcc的指令选项有:

  • -g(生成调试信息,GNU 调试器可利用该信息)
  • -ggdb(此选项将尽可能的生成 gdb 的可以使用的调试信息)

GDB中的基本调试命令

命令 命令缩写 命令说明
set args 设置主程序的参数。例如:./test 1 2设置参数的方法是:gdb test(gdb) set args 1 2
break b 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。
run r 开始运行程序, 程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。
next n 执行当前行语句,如果该语句为函数调用,不会进入函数内部执行。
step s 执行当前行语句,如果该语句为函数调用,则进入函数执行其中的第一条语句。注意了,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是您自定义的函数,只要有源码就可以进去。
print p 显示变量值,例如:p name表示显示变量name的值,print可以做运算。
continue c 继续程序的运行,直到遇到下一个断点。
set var name=value 设置变量的值,假设程序有两个变量:int ii; char name[21];set var ii=10 把ii的值设置为10;set var name=”abc” 把name的值设置为”abc”,注意,不是strcpy。
quit q 退出gdb环境。

进入GDB调试

1
2
3
4
gcc test.c -g -o test
gdb test # 进入gdb,指定test为可执行文件
# 也可以只执行gdb,在gdb中用file命令指定可执行文件
# 接下来就可以使用GDB的基本调试命令调试程序了

调试正在运行的程序

1
2
3
4
5
6
# 通过ps获取程序进程号
gdb test -p [进程号]
# 进入gdb后,程序会被暂停
(gdb)bt # 使用bt查看程序的调用栈
# Print backtrace of all stack frames, or innermost COUNT frames.
# 退出后,程序继续执行

调试多进程程序

1
2
3
4
5
6
(gdb)set follow-fork-mode [parent|child]
# (缺省是parent)调试父|子进程,父|子进程不受影响
(gdb)set datach-on-fork [on|off]
# (缺省是on)on表示调试当前进程时,其他进程继续运行,off表示调试当前进程时,其他进程被GDB挂起
(gdb)info inferiors # 查看调试的进程
(gdb)inferior # 切换当前调试的进程,进程id是查看调试的进程返回的Num值

调试多线程程序

1
2
3
4
5
6
7
8
ps aux|grep test # 查看当前运行的进程,查找符合"test"的字符串
ps -aL|grep test # 查看当前运行的轻量级进程
# 轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。
pstree -p [主线程ID] # 查看主线程和新线程的关系
(gdb)info threads # 查看线程
(gdb)thread [线程ID] # 切换线程,线程ID是查看线程返回的Num值
(gdb)set scheduler-locking [on|off] # on:只运行当前线程,off:运行全部的线程
(gdb)thread apply [线程ID|all] [cmd] # 让某个|全部线程执行某GDB命令

服务程序运行日志

设置断点或单步跟踪可能会严重干扰多进程、多线程之间的竞争状态,导致我们看到一个假象。一旦我们在某个线程设置了断点,该线程在断点处挺住了,只剩下另一个线程在跑。这时候,并发的场景已经完全被破坏了,通过调试器看到的只是一个和谐的场景(理想状态)。输出Log日志可以避免断点和单步跟踪所导致的副作用。

今晨的日出

220225-asmInC-1.jpg

早晨学习操作系统,看到系统调用实现时,遇到了C内嵌汇编,所以补了下相关知识。

__asm__

补充资料:https://dirtysalt.github.io/html/gcc-asm.html

在内嵌汇编中,可以将C语言表达式指定为汇编指令的操作数,而且不用去管如何将C语言表达式的值读入哪个寄存器,以及如何将计算结果写回C 变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可,GCC会自动插入代码完成必要的操作。

完整的内嵌汇编格式:__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);

__asm__ 是GCC关键字asm的宏定义,asm用于声明这行代码是一个内嵌汇编表达式,任何内嵌的汇编表达式都以此关键字作为开头。

一个最简单的内嵌汇编:__asm__("nop"); 括号里的是汇编指令,表示运行空指令,一个机器周期。Instruction List 是汇编指令序列,它可以是空的。可以有多条汇编指令,需要将所有指令放在多对引号中,两条指令必须用换行或分号分开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int printk(const char *fmt, ...)
{
// ……
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $buf\n\t"
"pushl $0\n\t"
"call tty_write\n\t"
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (i):"ax","cx","dx");
// ……
}

可以看上面printk()的例子,首先push %fs保存这个指向用户段的寄存器,在最后pop %fs将其恢复,printk()的核心是调用tty_write()

Output Input 用来指定当前内联汇编语句的输出与输入,格式为形如“constraint”(variable)的列表(逗号分隔),constraint是限制字符,下面表中列出几个常用限制字符作用,更多用法见:GCC内嵌汇编一些限制字符串

限制字符 作用
a/b/c/d/s/d 将输入变量放入eax/ebx/ecx/edx/esi/edi
q 将输入变量放入eax,ebx,ecx,edx中的一个
r 输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个
=/+ 操作数在指令中是只写/读写类型的(输出操作数)

__volatile__

__volatile__ 是GCC关键字volatile的宏定义,在内联汇编中,它是可选的。使用它会向GCC声明不允许对该内联汇编优化,否则当使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。

编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux 提供了一个宏,barrier(),解决编译器的执行顺序问题,但对硬件无效。

有一种硬件级别的优化:内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。

Clobber/Modify

有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望GCC在编译时能够将这一点考虑进去。那么你就可以在Clobber/Modify域声明这些寄存器或内存。这种情况一般发生在一个寄存器出现在”Instruction List”,但却不是由Input/Output操作表达式所指定的,也不是在一些Input/Output操作表达式使用”r”约束时由GCC 为其选择的,同时此寄存器被”Instruction List”中的指令修改,而这个寄存器只是供当前内联汇编临时使用的情况。

例如:__asm__ ("mov R0, #0x34" : : : "R0");寄存器R0出现在”Instruction List中”,并且被mov指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定”R0”,以让GCC知道这一点。

因为你在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操作表达式使用”r”约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。但除此之外, GCC对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。所以如果你真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify 中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。

如果一个内联汇编语句的Clobber/Modify域存在”memory”,那么GCC会保证在此内联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个 时候寄存器中的拷贝已经很可能和内存处的内容不一致了。

这只是使用”memory”时,GCC会保证做到的一点,但这并不是全部。因为使用”memory”是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点。

1
2
3
4
5
6
7
8
9
int main(int __argc, char* __argv[]) 
{
int* __p = (int*)__argc;
(*__p) = 9999;
__asm__("":::"memory");
if((*__p) == 9999)
return 5;
return (*__p);
}

上面例子中,如果没有那条内联汇编语句,那个if语句的判断条件就完全是一句废话。GCC在优化时会意识到这一点,而直接只生成return 5的汇编代码,而不会再生成if语句的相关代码,而不会生成return (*__p)的相关代码。但你加上了这条内联汇编语句,它除了声明内存变化之外,什么都没有做。但GCC此时就不能简单的认为它不需要判断都知道 (*__p)一定与9999相等,它只有老老实实生成这条if语句的汇编代码,一起相关的两个return语句相关代码。

linux内核中内存屏障也是基于它实现,#define barrier() _asm__volatile_("" : : : "memory"),主要是保证程序的执行遵循顺序一致性。

Linux screen工具

功能介绍

Linux screen命令用于多重视窗管理程序。此处所谓的视窗,是指一个全屏幕的文字模式画面。通常只有在使用telnet登入主机或是使用老式的终端机时,才有可能用到screen程序。

语法

1
screen [-AmRvx -ls -wipe][-d <作业名称>][-h <行数>][-r <作业名称>][-s <shell>][-S <作业名称>]

参数说明

  • -A  将所有的视窗都调整为目前终端机的大小。
  • -d<作业名称>  将指定的screen作业离线。
  • -h<行数>  指定视窗的缓冲区行数。
  • -m  即使目前已在作业中的screen作业,仍强制建立新的screen作业。
  • -r<作业名称>  恢复离线的screen作业。
  • -R  先试图恢复离线的作业。若找不到离线的作业,即建立新的screen作业。
  • -s  指定建立新视窗时,所要执行的shell。
  • -S<作业名称>  指定screen作业的名称。
  • -v  显示版本信息。
  • -x  恢复之前离线的screen作业。
  • -ls或–list  显示目前所有的screen作业。
  • -wipe  检查目前所有的screen作业,并删除已经无法使用的screen作业。

常用命令

1
2
3
4
5
screen -ls  # 显示已创建的screen终端
screen -S <screen name> # 创建新的screen终端
screen -r <screen name> # 重连screen终端
screen -d <screen name> # 断开screen终端
screen -S <screen name> -X quit # 强制停止命令

screen快捷键

  • Ctrl -a c 创建新的视窗
  • Ctrl -a d 断开screen终端,任务还在运行
  • Ctrl -a k 删除当前视窗
  • Ctrl -a 空格 视窗切换
  • Ctrl -a ? 快捷键帮助
  • Ctrl -a : 命令模式,类似vi
  • Ctrl -a [ 复制模式,可以移光标上去看上面的打印

screen配置

1
wget https://github.com/hao0527/hao0527.github.io/blob/main/others/screencfg -O ~/.screenrc  # 一个好用的screen配置文件

上面这个配置指令有些问题,wget下载下来的是html,所以就手动复制下内容到~/.screenrc文件吧。

Linux vi/vim工具

功能介绍

所有的 Unix Like 系统都会内建 vi 文书编辑器,其他的文书编辑器则不一定会存在。vim 具有程序编辑的能力,可以主动的以字体颜色辨别语法的正确性,方便程序设计。

vi/vim使用

基本上 vi/vim 共分为三种模式,分别是命令模式(Command mode)输入模式(Insert mode)底线命令模式(Last line mode)

命令模式

220224-Linux实用工具-1.jpg

输入 作用
yy 复制游标所在的那一行。(常用)
dd 剪切游标所在的那一整行(常用),用 p/P 可以粘贴。
p/P p粘贴在光标行下面,P粘贴在光标行上面。
u 复原前一个动作。(常用)
[Ctrl]+r 重做上一个动作。(常用)
/word 向光标之下寻找一个名称为 word 的字符串。 (常用)
?word 向光标之上寻找一个字符串名称为 word 的字符串。
i, I 进入输入模式(Insert mode)
ZZ 如果修改过,保存当前文件,然后退出!效果等同于保存并退出

常用的还有替换功能:

  • :1,$s/word1/word2/g:%s/word1/word2/g,从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 !(常用)
  • :1,$s/word1/word2/gc:%s/word1/word2/gc,从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 ,加了c表示取代前显示提示字符给用户确认 (confirm) 是否需要取代。

输入模式

输入模式比较简单,可以使用键盘上的Home、End等功能键,按Esc退出输入模式到命令模式

底线命令模式

输入 作用
:w 将编辑的数据写入硬盘档案中(常用)
:q 离开 vi (常用)
:q! 若曾修改过档案,又不想储存,使用 ! 为『强制』离开不储存档案。
:wq 储存后离开,若为 :wq! 则为强制储存后离开 (常用)
:w [filename] 将编辑的数据储存成另一个档案(类似另存新档)
:r [filename] 在编辑的数据中,读入另一个档案的数据。亦即将 『filename』 这个档案内容加到游标所在行后面
:n1,n2 w [filename] 将 n1 到 n2 的内容储存成 filename 这个档案。
:! command 暂时离开 vi 到指令行模式下执行 command 的显示结果!例如
『:! ls /home』即可在 vi 当中察看 /home 底下以 ls 输出的档案信息!
:set nu/nonu 显示/取消行号

Linux tldr工具

功能介绍

一个比 –help 和 man 好用的查指令手册的工具,点我跳到tldr(too long don’t read)主页

安装

我使用sudo apt install tldr安装,然后mkdir -p ~/.tldr/tldr,再更新字典sudo git clone https://gitclone.com/github.com/tldr-pages/tldr.git ~/.tldr/tldr,国内使用这个镜像快。

推荐:官网上说可以使用npm或pip3安装。

使用

使用方式极其简单,tldr tldr你就可以查到tldr的使用手册。

Linux Samba工具

功能介绍

samba 是基于SMB协议(ServerMessage Block,信息服务块)的开源软件,samba也可以是SMB协议的商标。SMB是一种Linux、UNIX系统上可用于共享文件和打印机等资源的协议,这种协议是基于Client\Server型的协议,Client端可以通过SMB访问到Server(服务器)上的共享资源。当Windows是 Client,Ubuntu是服务器时,通过Samba就可以实现window访问Linux的资源,实现两个系统间的数据交互。samba服务程序已经成为在Linux系统和Windows系统之间共享文件的最佳选择,当然在Linux系统与Linux系统之间的文件共享也选择samba。

安装

在Ubuntu中安装sudo apt install samba,会自动安装其依赖组件。

安装后可用samba -V查看samba版本号,以确认安装完成。

使用

配置文件目录在/etc/samba/smb.conf,配置以下内容可共享home目录下用户文件夹。

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
# Un-comment the following (and tweak the other settings below to suit)
# to enable the default home directory shares. This will share each
# user's home directory as \\server\username
[homes]
comment = Home Directories
browseable = no

# By default, the home directories are exported read-only. Change the
# next parameter to 'no' if you want to be able to write to them.
read only = no

# File creation mask is set to 0700 for security reasons. If you want to
# create files with group=rw permissions, set next parameter to 0775.
create mask = 0775

# Directory creation mask is set to 0700 for security reasons. If you want to
# create dirs. with group=rw permissions, set next parameter to 0775.
directory mask = 0775

# By default, \\server\username shares can be connected to by anyone
# with access to the samba server.
# Un-comment the following parameter to make sure that only "username"
# can connect to \\server\username
# This might need tweaking when using external authentication schemes
valid users = %S

遇到的问题

以上配置后需要重启samba服务生效,可以注销用户后重新登录。

通过sudo smbpasswd -a userName添加一个samba用户,不然直接windows远程访问会拒绝访问。

userName需要是系统已有的用户名,否则会Failed to add entry for user userName

再次了解进程与线程

  • Python异步与线程:这是21年6月时对线程、进程、同步、异步的简单了解,最近在做毕设时又用到了,打算重新回顾一遍。

多线程与多进程的区别

多线程 threading: 一个人有与异性聊天和看剧两件事要做。单线程的她可以看完剧再去聊天,但这样子可能就没人陪她聊天了「哼,发消息不回」。我们把她看成一个CPU核心,为她开起多线程——先看一会剧,偶尔看看新消息,在两件事(线程)间来回切换。多线程:单个CPU核心可以同时做几件事,不至于卡在某一步傻等着。

用处:爬取网站信息(爬虫),等待多个用户输入

多进程 processing: 一个人有很多砖需要搬,他领取手套、推车各种物资(向系统申请了资源)然后开始搬砖。然而他身边有很多人,我们让这些人去帮他!(一核有难,八核围观)。于是他们做了分工,砖很快就搬完了。多进程让多个CPU核心可以一起做事,不至于只有一人干活而其他人傻站着。

用处:进行高性能计算。只有多进程方案设计合理,才能加速计算。

Python中应用多进程

  • 毕设的上位机要接收显示图像,我在udp数据接收解码后,写入IO流,通过管道发给另一个进程,另一个进程做图像的处理与显示,这样就不会因为处理时间过长而阻塞下一次接收解码了。
  • multiprocessing — 基于进程的并行:multiprocessing是Python自带的多进程库,这是Python官方的文档对multiprocessing库的介绍。
  • Python的线程是操作系统线程,因此要有Python全局解释器锁。一个python解释器进程内有一条主线程,以及多条用户程序的执行线程。即使在多核CPU平台上,由于GIL的存在,所以禁止多线程的并行执行。Python 3.6 才让multiprocessing逐渐发展成一个能用的Python内置多进程库,可以进行进程间的通信,以及有限的内存共享。

两种多进程创建方式

Python多进程可以选择两种创建进程的方式,spawn与fork,实际使用中可以根据子进程具体做什么来选取用fork还是spawn。

  1. fork:除了必要的启动资源外,其他变量,包,数据等都继承自父进程,并且是copy-on-write的,也就是共享了父进程的一些内存页,因此启动较快,但是由于大部分都用的父进程数据,所以是不安全的进程
  2. spawn:从头构建一个子进程,父进程的数据等拷贝到子进程空间内,拥有自己的Python解释器,所以需要重新加载一遍父进程的包,因此启动较慢,由于数据都是自己的,安全性较高
1
2
multiprocessing.set_start_method('spawn')  # default on WinOS or MacOS
multiprocessing.set_start_method('fork') # default on Linux (UnixOS)

四种进程间通信方式

Python中进程间通信可以采用进程池Pool、管道Pipe、队列Queue,在新版本Python中多了共享内存Manager的方式。

  1. 进程池Pool:不怎么使用?通常使用另外两个方式通信
  2. 管道和队列:Queue用于多个进程间实现通信,Pipe是两个进程的通信。Queue通过put和get方法插入读取队列,Pipe通过send和recv方法发送和接收信息,用法可以参考这篇文章:python进程间通信,如果追求运行更快,那么最好使用管道Pipe而队列Queue,详细查看Python pipes and queues performance
  3. 共享内存Manager:Pipe Queue 把需要通信的信息从内存里深拷贝了一份给其他线程使用(需要分发的线程越多,其占用的内存越多)。而共享内存会由解释器负责维护一块共享内存(而不用深拷贝),这块内存每个进程都能读取到,读写的时候遵守管理(因此不要以为用了共享内存就一定变快)

编程中要注意

为了避免自己调用自己时重复执行主进程,**多进程的主进程一定要写在程序入口if __name__ == ‘__main__’:,否则可能会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
def function1(id):  # 这里是子进程
print(f'id {id}')

def run__process(): # 这里是主进程
from multiprocessing import Process
process = [mp.Process(target=function1, args=(1,)),
mp.Process(target=function1, args=(2,)), ]
[p.start() for p in process] # 开启了两个进程
[p.join() for p in process] # 等待两个进程依次结束

# run__process() # 主线程不建议写在 if外部。由于这里的例子很简单,你强行这么做可能不会报错
if __name__ =='__main__':
run__process() # 正确做法:主线程只能写在 if内部

设计高性能的多进程时,遵守以下规则:

  • 尽可能少传一点数据
  • 尽可能减少主线程的负担
  • 尽可能不让某个进程傻等着
  • 尽可能减少进程间通信的频率

网上一些好的文档

大学回忆录

大一上学期

回忆从开学前几日的寝室生活开始,室友都非常的好相处,大家在一起能玩的很嗨。再到大一我参加了两个学生会的部门(学生会办公室&党员之家实践部)和一个社团(单车俱乐部),自己在部门里认识了些不同班级不同专业的朋友,也交到了一位知心,大一上学期我天天晚上会和他去健身房,或是去钱塘江边散步,或是在寝室喝酒,元旦跨年我去了他家里,人生中第一次和别人跨年。参与了学生会的活动演出和演讲还有优秀干部的竞选;

大一寒假

寒假里我一个人坐飞机去了山东蓬莱,找我高考结束后在华为上班认识的同事,蓬莱待了7天,青岛待了2天。独自旅行,去一个陌生的地方,享受孤独,思考人生,一个人狂欢。旅行结束回家后,我学习了郭天祥的十天学会51单片机,但并没有完全学会;

大一下学期

大一下学期有我喜欢的课程模电,我通过教同学们解题来学习这门课程,因此也交到了陪伴我至今的女朋友。除了模电课我还选了一门电子课程设计也非常喜欢,老师授课讲了51单片机的知识,课程做了51数字钟(我在5月19日晚上写完,拍了个51开发版显示520倒计时的视频),最后的作业是51小车。这学期学生会部门成员积极性变差了,我最后没有选择留任执委,选择了留在电子创新实验室(一待就是三年);

大一暑假

暑假我留在实验室准备19年的全国大学生电子设计竞赛,暑假做了历年的题目(声音存储录放、风力摆、旋转倒立摆),那时候的我不知道熬夜会伤身体,每天白天调试,晚上在寝室看文档到凌晨2点。暑假里由于比较专注比赛,忘记关心女朋友,两人有过多次争执,女朋友不回消息,我直接去她家里了,就这样被她家人认识,至今我还常去她家蹭饭。这个暑假差一点点就分手了;

大二上学期

大二上学期,一开学和徐高东学长参加了工程训练大赛,那次比赛后感觉之前所有学过的东西都能联会贯通了。比赛的时候出现了意外,没有取得好成绩,东哥说了句:比赛不就是这样。随后我参加了飞思卡尔智能车竞赛,在校内训练的时候感觉都良好,本来计划我和洪晨益写程序,由于硬件画的板子实在惨不忍睹,就自己动手把硬件做完了,我那写代码的队友也很强,一人能维护好代码,那年寒假前我们俩调车调到学校关门。这学期,我和我的女朋友分手了,但几个星期又复合了,我们相互都很缺时间在一起交流;

大二寒假

那年是全世界肺炎流行的一年,寒假在家3个月,当然我也没闲着,趁家里无聊,打开电脑学了些东西,有python,tensorflow,sklearn,百度飞桨,git等,有潘利斌陪我一起学。疫情原因只能在家调飞卡,在家里把Altium Designer了一遍,画板子是没有问题了。3月在家网课,网课非常适合我,我可以不仅可以回放自己学校老师上课讲的,还能去B站慕课上找名校老师上课的视频;

大二下学期

中国疫情控制的很厉害,4月中就能返校了。这学期在学校主要是准备电赛,准备飞卡,飞卡参加的是AI组别,通过机器学习,训练后部署到单片机,实现自主规划路线;

大二暑假

暑假培训大一电赛新生,不过今年比赛延期了,暑假没有比赛。飞卡如期举行,离国赛就差0.02秒,怎么就这么可惜呀。电赛准备了很多模块,把模块间通信学的明明白白的,还有很多PCB上的设计规范也学了一遍,那年我们实验室都在看长江大学唐老师的教学视频。

大三上学期

大三上学期开始在黄道麒公司实习,老板非常厉害,抓住了疫情这个风口,做红外热像仪体温枪这些生意。公司离学校很近,我每天上完课就去公司,那段时间进步非常快,学的东西也非常多非常杂。用了好多品牌的国产mcu,用QT写了上位机,学习了二元光学。十月份浙江省电子大赛,作品实物验收满分取得了省二等奖,队友配合的非常棒!这学期上了算法与数据结构和数字逻辑设计、51汇编,非常感兴趣,买了块FPGA开发板练手;

大三寒假

寒假里我还在公司上班,老板让我住宾馆可以报销。寒假里我学习了linux应用开发,opencv,makefile,cmake,用树莓派做了个红外热像仪的demo。这个寒假我在公司借了很多书看,有linux的,opencv的,无线传感器网络的,感测技术的。在公司里和老板经常聊到很晚,我们谈未来的方向,谈生活时政等等,无所不谈,是一家有温度的公司

大三下学期

这学期了解了一些网络安全,开关电源设计,搭建了blog,仍然在公司实习。搞的东西太杂了容易忘记自己学过什么,写写博客记录下的话翻到还能有印象。公司里主要是把之前用树莓派跑的demo移到了m4的mcu,另一个项目是用高云fpga做了ov2640的串并转换到mcu处理,练习了fpga。5月份之前飞卡的软件队友提出来要再参加一次,让我做一下硬件,这次我非常熟练,PCB最多就设计了两版就完成了硬件设计;

大三暑假

暑假去了一家做半导体芯片的公司实习,是属于系统集成部,公司平均年龄比较大,部门里同事都不怎么说话,公司氛围不太好,领导经常会骂别人很凶,我一看不对劲就溜溜球了。暑假在做wifi图传的项目,尝试用m4自己看数据手册写WiFi驱动,不过后来这事没成,换了esp32用idf开发。很早开始我就对网络感兴趣,可能未来也会多往这个方向发展;

大四上学期

大四第一学期在世界五百强博世上班,公司的氛围特别好,领导都很有管理的能力,同事之间办公都非常的舒心,博世是个不加班的外企,但是工作效率挺高的,流程虽多,但处理速度很快。我在博世工作了4个月,学习了高压交流的PCB设计,直流无刷电机,永磁同步电机,把电力这一块学了一遍,也了解了电机的控制算法,最简单的六步换向,还有FOC。可以说这次实习经历相当难忘,每个牛的公司,都有一群好的团队管理者。这学期,还报名考研了,算是把大学的知识复习了一下,没考好,以后也不会再有考研的想法了;

还有最后一个假期

最后一个假期也没选择安逸,找了家做安防的公司实习,在这里我学到了linux驱动开发的知识,我的主管年龄挺大的技术很好,整个框架是他从头写出来的,我看git log一步步看他怎么写出来,太强了!面试的时候就被他的技术所吸引,这么强的人为人还非常谦虚,值得我去学习!在这里我重学了一年前了解过的makefile编写,shell脚本编程,C代码规范等等。看了主管写的几万行脚本代码,我再感叹到太强了!过年在家里自己系统的学了一遍计算机网络,本来还想学计算机组成原理和操作系统的,学操作系统的时候卡住了,看早期的linux源码也非常的费劲,只能推到我2月实习完回学校再学了;

致青春:如果没有源自内心的冲动,怎么可能登峰造极呢?

学习视频

以太网的介绍

网络体系结构

220113-fpga之eth-1.jpg

  • 物理层:在物理层上所传数据的单位是比特。发送方发送1(或0)时,接收方应当收到1(或0)而不是0 (或1),因此物理层要考虑用多大的电压代表”1”或”0”,以及接收方如何识别出发送方所发送的比特。物理层还要确定连接电缆的插头应当有多少根引脚以及各引脚应如何连接。当然,解释比特代表的意思,就不是物理层的任务。请注意,传递信息所利用的-些物理媒体,如双绞线、同轴电缆、光缆、无线信道等,并不在物理层协议之内而是在物理层协议的下面。
  • 数据链路层:数据链路层常简称为链路层。我们知道,两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。在两个相邻结点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧(framing),在两个相邻结点间的链路上传送帧(frame),每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制等)。在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结·束。这样,数据链路层在收到一个帧后,就可从中提取出数据部分,上交给网络层。控制信息还使接收端能够检测到所收到的帧中有无差错。如发现有差错,数据链路层就简单地丢弃这个出了差错的帧,以免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在数据链路层传输时出现的差错(这就是说,数据链路层不仅要检错,而且要纠错),那么就要采用可靠传输协议来纠正出现的差错。这种方法会使数据链路层的协议复杂些。
  • 网络层、运输层、应用层见《计算机网络》书籍;fpga跑eth主要用到了phy层、数据链路mac层、网络层、udp传输层;

FPGA数据包内容

下面放出fpga发送的以太网数据包的组层图,图片来自《开拓者FPGA 开发指南V1.3》,相关介绍也可以查看PDF。

以太网数据包格式

220113-fpga之eth-2.jpg

以太网帧格式

220113-fpga之eth-3.jpg

IP数据包格式

220113-fpga之eth-4.jpg

UDP数据格式

220113-fpga之eth-5.jpg

千兆以太网物理层

  • 下面这个是我开发板图和板上的千兆以太网原理图,设计的非常好,板子上的IO基本上都是等长线。

220113-fpga之eth-6.jpg

220113-fpga之eth-7.jpg

  • 物理层的设计和学习还有个非常重要的学习资料就是《RTL8211数据手册》,这个手册上竟然写着不对外公布。

调试过程

例程修改

  • 简易的修改了下例程,让udp每次数据包发送1472个字节数据,其中前4字节是包序列号,用于接收端检测是否掉包,其他的1468个字节数据为0x00-0xFF循环,从而更为精确的模仿正常传输时的内容。再通过每次发完控制idle时间来控制传输速率。

上位机编写

  • 上位机才用Python编写,在JupyterNotebook里实现。实现原理主要就是一直读udp,再去比较这次的包序列号是不是上一次+1,如果不是的话打印出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import socket
BUFSIZE = 1472 * 100
ip_port = ('192.168.0.3', 8080)
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp协议
server.bind(ip_port)
error = 0
recv_data_a = server.recvfrom(BUFSIZE)[0][:4]
while True:
recv_data_b = server.recvfrom(BUFSIZE)[0][:4]
if(int.from_bytes(recv_data_b,byteorder='big',signed=False) != \
int.from_bytes(recv_data_a,byteorder='big',signed=False) + 1):
error += 1
print("error:%u, recv_data_a = %u, recv_data_b = %u" \
%(error, int.from_bytes(recv_data_a,byteorder='big',signed=False), \
int.from_bytes(recv_data_b,byteorder='big',signed=False)))
recv_data_a = server.recvfrom(BUFSIZE)[0][:4]
if(int.from_bytes(recv_data_a,byteorder='big',signed=False) != \
int.from_bytes(recv_data_b,byteorder='big',signed=False) + 1):
error += 1
print("error:%u, recv_data_b = %u, recv_data_a = %u" \
%(error, int.from_bytes(recv_data_b,byteorder='big',signed=False), \
int.from_bytes(recv_data_a,byteorder='big',signed=False)))
server.close()

掉包处理

  • 在100Mbps速率传输的时候,每65536帧传输后都会出现2帧的掉包,通过WireShark查看,能找到那2帧包,也就是说数据包被网卡接收但是在上位机处没有接收。通过WireShark查看整个以太网数据包内容后发现规律:只有在IP数据包的首部校验和为0xFFFF的时候才会出现掉包,百度上也有这样的案例,于是修改FPGA代码,让IP包首部标识位不每次+1,这样校验和就不会出现0XFFFF。

调试结果

  • 经过简易测试,当传输速率为200Mbps的时候,不掉包;当传输速率为300Mbps的时候,出现掉包。

总结

  • 本次调试,学习了网络5层模型的物理层和MAC层,通过亲自实践解决出现的问题,对网络的底层实现有了更清楚的认识,对日后学习Linux驱动开发的网络部分会有极大的帮助,以及我的毕设,udp传输图像只是其中的一小部分,如果有时间,我会做个更复杂题目,学习更多的网络知识(大学只学了物联网和网络相关,计网没学)。

Linux-File(文件写入)

​ 对于write函数,我们认为该函数一旦返回,数据便已经写到了文件中。但是这种概念只是宏观上的,一般情况下,对硬盘(或者其他持久存储设备)文件的write操作,更新的只是内存中的页缓存(page cache),而脏页不会立即更新到硬盘中,而是由操作系统统一调度,如flusher内核线程在满足一定条件时(一定时间间隔、内存中的脏页达到一定比例)将脏页面同步到硬盘上(放入设备的IO请求队列)。因为write调用不会等到硬盘IO完成之后才返回,设想如果操作系统在write调用之后、硬盘同步之前崩溃,则数据可能丢失。虽然这样的时间窗口很小,但是对于需要保证事务的持久化(durability)和一致性(consistency)的数据库程序来说,write()所提供的“松散的异步语义”是不够的,通常需要操作系统提供的同步IO(synchronized-IO)原语来保证:

​ Linux、unix在内核中设有缓冲区、高速缓冲或页面高速缓冲,大多数磁盘I/O都通过缓冲进行,采用延迟写技术。
sync:将所有修改过的快缓存区排入写队列,然后返回,并不等待实际写磁盘操作结束;
fsync:只对有文件描述符制定的单一文件起作用,并且等待些磁盘操作结束,然后返回;
fdatasync:类似fsync,但它只影响文件的数据部分。fsync还会同步更新文件的属性;
fflush:标准I/O函数(如:fread,fwrite)会在内存建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其写入磁盘,还需要调用fsync。(先调用fflush后调用fsync,否则不起作用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pFile = fopen(pFilePath, "wb");
if (NULL == pFile)
{
return FAILURE;
}
res = fwrite(aValStr, 1, strlen(aValStr), pFile);
if(res != strlen(aValStr))
{
return FAILURE;
}
fflush(pFile); //刷新缓冲区,强制缓冲区文件内容写入内核缓冲
fsync(fileno(pFile)); //同步内存中已修改的对应fd的文件数据到设备存储
//fileno()用于返回文件流对应的文件描述符fd
fclose(pFile);

Linux-Net(五种IO模型)

阻塞IO模型

  • 当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。

非阻塞IO模型

  • 当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
  • 所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU,如果在while循环体中一直去询问内核数据是否就绪,就会导致CPU占用率非常高。

多路复用IO模型

  • 在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
  • 也许有朋友会说,我可以采用 多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。
  • 不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。(才用多线程或线程池的方式来轮询和处理时间可解决)

信号驱动IO模型

  • 在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

异步IO模型

  • 异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它收到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了
  • 也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。
  • 前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。