基本概念:
CPU调度
和分派
的基本单位,用于保证程序的实时性,实现进程内部的并发;最小执行和调度单位
。Socket
, 文件句柄什么的)区别:
进程间通信IPC
主要包括: 管道, 系统IPC
(包括消息队列、信号量、信号、共享内存等) 以及套接字socket
。进程间通信主要包括管道
、系统IPC
(包括消息队列
、信号量
、信号
、共享内存
等)、以及套接字socket
。
PIPE
:
read
、write
等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。FIFO
:
FIFO
可以在无关的进程之间交换数据FIFO
有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。IPC
队列ID
)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;semaphore
IPC
结构不同,它是一个计数器
,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。PV
操作,程序对信号量的操作都是原子操作。PV
操作不仅限于对信号量值加 1
或减 1
,而且可以加减任意正整数。信号signal
: 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。共享内存(Shared Memory)
:它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等
IPC
,因为进程是直接对内存进行存取SOCKET
:
socket
也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。线程间通信的方式:
临界区
:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;互斥量Synchronized/Lock
:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问信号量Semphare
:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。事件(信号) Wait/Notify
:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作Linux
虚拟地址空间malloc
时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。物理地址
远小于逻辑地址
!!)32位逻辑地址
寻址36位的物理地址
)寻址空间
和可寻址空间
磁盘I/O
,这是很耗时的任何程序程序本质上都是由BSS段
、data段
、text段
三个组成的。
BSS段(未初始化数据区)
:通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段
属于静态分配,程序结束后静态变量资源由系统自动释放。数据(data)段
:存放程序中已初始化的全局变量, 静态变量以及常量数据的一块内存区域。数据段也属于静态内存分配。代码(text)段
:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量(例如字节型字面值常量)可执行程序在存储(没有调入内存)时分为代码段
、数据区
和未初始化数据区
三部分。
text段
和data段
在编译时已经分配了空间,而BSS段
并不占用可执行文件的大小(因为没有初值),它是由链接器来获取内存的。bss段
(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0
。需要存放在程序文件中的只有正文段和初始化数据段。data段
(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。数据段
包含经过初始化的全局变量以及它们的值。BSS段
的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。数据段
和BSS段
的整个区段此时通常称为数据区(静态区)
。可执行程序在运行时又多出两个区域:栈区
和堆区
。
1M
)是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。BSS
和栈
中间的地址区域。由程序员申请分配和释放。malloc/free
造成内存空间的不连续,产生碎片。malloc()
和mmap()
等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存.保护 CPU 现场
恢复 CPU 现场
,继续执行fork
和vfork
的区别fork
:创建一个和当前进程映像一样的进程可以通过fork( )
系统调用:#include
#include
pid_t fork(void);
fork( )
会创建一个新的进程,它几乎与调用fork( )
的进程一模一样,这两个进程都会继续运行。
fork( )
调用会返回0
。fork( )
返回子进程的pid
。fork( )
返回一个负值。fork( )
用法是创建一个新的进程,然后使用exec( )
载入二进制映像,替换当前进程的映像。
fork
派生了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。早期的Unix系统
中,创建进程比较原始。当调用fork
时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。Unix系统
采取了更多的优化,例如Linux
,采用了写时复制的方法,而不是对父进程空间进程整体复制。vfork
:在实现写时复制之前,Unix
的设计者们就一直很关注在fork
后立刻执行exec
所造成的地址空间的浪费。BSD
的开发者们在3.0
的BSD
系统中引入了vfork( )
系统调用。#include
#include
pid_t vfork(void);
exec
的系统调用,或者调用_exit( )
退出,对vfork( )
的成功调用所产生的结果和fork( )
是一样的。vfork( )
会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像。
vfork( )
避免了地址空间的按页复制。vfork( )
只完成了一件事:复制内部的内核数据结构。因此,子进程也就不能修改地址空间中的任何内存。vfork( )
是一个历史遗留产物,Linux
本不应该实现它。需要注意的是,即使增加了写时复制,vfork( )
也要比fork( )
快,因为它没有进行页表项的复制。然而,写时复制的出现减少了对于替换fork( )
争论。2.2.0
内核,vfork( )
只是一个封装过的fork( )
。因为对vfork( )
的需求要小于fork( )
,所以vfork( )
的这种实现方式是可行的。Linux
采用了写时复制的方法,以减少fork
时对父进程空间进程整体复制带来的开销。Copy-On-Write
)是以页为基础进行的。所以,只要进程不修改它全部的地址空间,那么就不必复制整个地址空间。在fork( )
调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的父进程或子进程共享。(MMU)
提供了硬件级别的写时复制支持,所以实现是很容易的。fork( )
时,写时复制是有很大优势的。因为大量的fork
之后都会跟着执行exec
,那么复制整个父进程地址空间中的内容到子进程的地址空间完全是在浪费时间:如果子进程立刻执行一个新的二进制可执行文件的映像,它先前的地址空间就会被交换出去。写时复制可以对这种情况进行优化。fork
和vfork
的区别:
fork( )
的子进程拷贝父进程的数据段和代码段;vfork( )
的子进程与父进程共享数据段fork( )
的父子进程的执行次序不确定;vfork( )
保证子进程先运行,在调用exec
或exit
之前与父进程数据是共享的,在它调用exec
或exit
之后父进程才可能被调度运行。vfork( )
保证子进程先运行,在它调用exec
或exit
之后父进程才可能被调度运行 。 如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会 导致死锁。linux
默认最大文件句柄数是1024
个,在linux
服务器文件并发量比较大的情况下,系统会报"too many open files"
的错误。故在linux
服务器高并发调优时,往往需要预先调优Linux
参数,修改Linux
最大文件句柄数。
有两种方法:
ulimit -n <可以同时打开的文件数>
,将当前进程的最大句柄数修改为指定的参数
shell
或者重新开启一个进程,参数还是之前的值首先用 ulimit -a 查询`Linux`相关的参数,如下所示:
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 94739
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 94739
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
其中,open files就是最大文件句柄数,默认是1024个。
修改Linux最大文件句柄数: ulimit -n 2048, 将最大句柄数修改为 2048个。
Linux系统参数
vi /etc/security/limits.conf
添加* soft nofile 65536
* hard nofile 65536
// 将最大句柄数改为 65536
并发(concurrency)
:指宏观上看起来两个程序在同时运行,比如说在单核cpu
上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。并行(parallelism)
:指严格物理意义上的同时运行,比如多核cpu
,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu
都是往多核方面发展。MySQL
的端口号是多少,如何修改这个端口号show global variables like 'port';
查看端口号mysql
的默认端口是3306
。sqlserver
默认端口号为:1433
;oracle
默认端口号为:1521
;DB2
默认端口号为:5000
;PostgreSQL
默认端口号为:5432
/etc/my.cnf
文件,早期版本有可能是my.conf
文件名,增加端口参数,并且设定端口,注意该端口未被使用,保存退出。页式内存管理,内存分成固定长度的一个个页片。
Linux
最初的两级页表机制:
32位
的虚拟空间分成三段,分别表示页目录表项
, 页表项
, 内页偏移
, 虚拟地址高10位表示页目录表偏移, 中间10位表示页表偏移, 低12位表示页内偏移页内偏移
,高20分成两段分别表示两级页表的偏移。
PGD(Page Global Directory)
: 最高10位,全局页目录表索引PTE(Page Table Entry)
:中间10位,页表入口索引4K
, 页表项索引的大小为4bites
, 所以一页中可以存放1024(2 10 ^{10} 10)个页表项
1024
个页表项索引1024
页索引CR3寄存器
中存放的全局页目录表(page directory, PGD)
的这一页的物理地址高10位
叫做页目录表项(内核也称这为pgd
)的部分作为偏移, 即定位到可以描述该地址的pgd
;从该pgd
中可以获取可以描述该地址的页表的物理地址,抽取中间10位
作为偏移, 即定位到可以描述该地址的pte
;pte
中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位
,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。page talbe walk
。Linux
的三级页表机制:
X86
引入物理地址扩展(Pisycal Addrress Extension, PAE)
后,可以支持大于4G
的物理内存(36位),但虚拟地址依然是32位,原先的页表项不适用,它实际多4 bytes
被扩充到8 bytes
,这意味着,每一页现在能存放的pte
数目从1024
变成512了(4k/8)
。相应地,页表层级发生了变化,Linux新增加了一个层级,叫做页中间目录(page middle directory, PMD)
, 变成:段 | 描述 | 位数 |
---|---|---|
r3 | 指向一个PDPT | crs寄存器存储 |
GD | 指向PDPT中4个项中的一个 | 位31~30 |
MD | 指向页目录中512项中的一个 | 位29~21 |
TE | 指向页表中512项中的一个 | 位20~12 |
age offset | 4KB页中的偏移 | 位11~0 |
Linux
采取了一种抽象方法:所有架构全部使用3级页表: 即PGD -> PMD -> PTE
。PMD
抽象掉,即虚设一个PMD表项
。这样在page table walk
过程中,PGD
本直接指向PTE
的,现在不了,指向一个虚拟的PMD
,然后再由PMD
指向PTE
。这种抽象保持了代码结构的统一。Linux
的四级页表机制:
64位CPU
出现了, 比如X86_64
, 它的硬件是实实在在支持4级页表的。它支持48位
的虚拟地址空间。如下:段 | 描述 | 位数 |
---|---|---|
ML4 | 指向一个PDPT | 位47~39 |
GD | 指向PDPT中4个项中的一个 | 位38~30 |
MD | 指向页目录中512项中的一个 | 位29~21 |
TE | 指向页表中512项中的一个 | 位20~12 |
age offset | 4KB页中的偏移 | 位11~0 |
(PGD->PMD->PTE)
,做了折衷。即采用一个唯一的,共享的顶级层次,叫PML4
。这个PML4
没有编码在地址中,这样就能套用原来的3级列表方案了。不过代价就是,由于只有唯一的PML4, 寻址空间被局限在(239=)512G, 而本来PML4段有9位, 可以支持512个PML4表项的。现在为了使用3级列表方案,只能限制使用一个, 512G的空间很快就又不够用了,解决方案呼之欲出。X86_64
架构代码的维护者Andi Kleen
提交了一个叫做4level page tables for Linux
的PATCH
系列,为Linux
内核带来了4级页表的支持。在他的解决方案中,不出意料地,按照X86_64规范,新增了一个PML4的层级, 在这种解决方案中,X86_64
拥一个有512
条目的PML4
, 512条目的PGD, 512条目的PMD, 512条目的PTE。对于仍使用3级目录的架构来说,它们依然拥有一个虚拟的PML4,相关的代码会在编译时被优化掉。 这样,就把Linux内核的3级列表扩充为4级列表。这系列PATCH工作得不错,不久被纳入Andrew Morton的-mm树接受测试。不出意外的话,它将在v2.6.11版本中释出。但是,另一个知名开发者Nick Piggin提出了一些看法,他认为Andi的Patch很不错,不过他认为最好还是把PGD作为第一级目录,把新增加的层次放在中间,并给出了他自己的Patch:alternate 4-level page tables patches。Andi更想保持自己的PATCH, 他认为Nick不过是玩了改名的游戏,而且他的PATCH经过测试很稳定,快被合并到主线了,不宜再折腾。不过Linus却表达了对Nick Piggin的支持,理由是Nick的做法conceptually least intrusive
。毕竟作为Linux的扛把子,稳定对于Linus来说意义重大。最终,不意外地,最后Nick Piggin的PATCH在v2.6.11版本中被合并入主线。在这种方案中,4级页表分别是:PGD -> PUD -> PMD -> PTE
。进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;
但是其具有一些缺点:
因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性。
和进程相比,线程的优势如下:
linux
系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:
CPU系统
更加有效。操作系统会保证当线程数不大于CPU数目
时,不同的线程运行于不同的CPU
上。ID
、线程状态
、栈
、寄存器状态等信息
。其中寄存器主要包括SP
PC
EAX
等寄存器,SP
:栈指针,指向当前栈的栈顶地址PC
:程序计数器,存储下一条将要执行的指令EAX
:累加寄存器,用于加法乘法的缺省寄存器信号量
P(SV)
:如果信号量SV
大于0,将它减一;如果SV
值为0,则挂起该线程。V(SV)
:如果有其他进程因为等待SV
而挂起,则唤醒,然后将SV+1
;否则直接将SV+1
。sem_wait(sem_t *sem)
: 以原子操作的方式将信号量减1
,如果信号量值为0
,则sem_wait
将被阻塞,直到这个信号量具有非0值。sem_post(sem_t *sem)
: 以原子操作将信号量值+1
。当信号量大于0
时,其他正在调用sem_wait
等待信号量的线程将被唤醒。互斥量
pthread_mutex_init
:初始化互斥锁pthread_mutex_destroy
:销毁互斥锁pthread_mutex_lock
:以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,pthread_mutex_lock
调用将阻塞,直到该互斥锁的占有者将其解锁。pthread_mutex_unlock
:以一个原子操作的方式给一个互斥锁解锁。条件变量
signal/broadcast
。此时操作共享变量时需要加锁。pthread_cond_init
:初始化条件变量pthread_cond_destroy
:销毁条件变量pthread_cond_signal
:唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。pthread_cond_wait
:等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。线程控制块(tcb)
,线程Id
,栈
、寄存器
。TLB(Translation Lookaside Buffer, 页表缓存)
以使用新的地址空间;TLB
。OS
缺页置换算法先进先出(FIFO)
算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。最近最少使用(LRU)
算法: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。LRU
算法。CPU
, 适用于CPU密集型
。同时,多进程模型也适用于多机分布式场景中,易于多机扩展。I/O密集型
的工作场景,因此I/O密集型
的工作场景经常会由于I/O阻塞
导致频繁的切换线程。同时,多线程模型也适用于单机多核分布式场景。物理地址(physical address):
虚拟地址(virtual memory)
地址转换: 参考 总结一下linux中的分段机制
第二步 页式管理——线性地址转物理地址: 再利用其页式内存管理单元,转换为最终物理地址。
linux
假的段式管理
intel
硬件的要求。Linux
也需要提供一个高层抽像,来提供一个统一的界面。Linux
的段式管理,事实上只是“哄骗”了一下硬件而已。按照Intel
的本意,全局的用GDT
,每个进程自己的用LDT
——不过Linux
则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。(主要原因是, 分段可以把每一个进程分配不同的线性地址空间,而分页则可以把相同的线性地址空间映射到不同的物理空间)Linux
下,逻辑地址与线性地址总是一致的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的, 映射到不同的物理空间是由页管理机制实现的。linux
页式管理
cr3
寄存器中。Base
子段就是段的起始地址(段开始位置的线性地址).段起始地址 + 段内偏移量 = 线性地址
(GDT)
: 一些全局的段描述符就放在全局段描述符表(GDT)
中,GDT
在内存中的地址和大小存放在CPU
的gdtr
控制寄存器中(LDT)
: 每个进程都有自己的段描述符就存放在局部段描述符中,LDT
则在ldtr
寄存器中。Linux
中,只是引入了段式管理的概念,并没有严格遵守. 它对所有的用户态进程都是用相同的段来进行指令和数据的寻址. 即所有用户态进程是使用相同的用户数据段和用户代码段, 所有的内核态进程使用想偶通的内核数据段和内核代码段.Linux
逻辑地址等于线性地址, 每个进程维护了自己的页表, 再多级分页机制可以把相同的线性地址空间映射到不同的物理空间.Linux
中,只是引入了段式管理的概念,并没有严格遵守. 不仅简化了linux
内核的设计,而且为了把linux
移植到其他平台创造了条件,因为很多RISC
处理器并不支持段机制。linux
内核中内存管理中:对整个逻辑地址空间j进行了划分(用户态0-3G, 内核态3-4G), 所有用户态的进程的段基地址均为0
,即每个段的逻辑地址与线性地址保持一致, 然后每个进程维护了各自的页表, 从而实现了不同进程中的小童线性地址到不同物理地址的映射.struct
)(或联合(union)
)的数据成员,第一个数据成员放在offset
为0
的地方,以后每个数据成员的对齐按照#pragma pack
指定的数值和这个数据成员自身长度中,比较小的那个进行。#pragma pack
指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。#pragma pack(n)
,n=1,2,4,8,16
来改变这一系数,其中的n
就是指定的“对齐系数”。#pragma pack(2)
struct AA {
int a; //长度4 > 2 按2对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 2 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 = 2 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
char d; //长度1 < 2 按1对齐;偏移量为7;存放位置区间[8];共九个字节
};
#pragma pack()
offset
为0
的地址处,以后每个数据成员要对齐至 min(pragma pack
指定的数值, 自身)pragma pack
, max(数据成员));进程间通信主要包括管道
、系统IPC
(包括消息队列
、信号量
、信号
、共享内存
等)、以及套接字socket
。
管道:
PIPE
:FIFO
:系统IPC
:
信号量semaphore
IPC
结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。信号(signal
)
共享内存(Shared Memory
)
IPC
,因为进程是直接对内存进行存取套接字SOCKET
:
socket
也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。比较常见的内存替换算法有:FIFO
,LRU
,LFU
,LRU-K
,2Q
。
FIFO
(先进先出淘汰算法)
LFU
(最不经常访问淘汰算法)
LRU
(最近最少使用替换算法)
LRU-K(LRU-2、LRU-3)
2Q
CPU
调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;Synchronized/Lock
:
Semphare
:
Wait/Notify
:
进程同步方式: 信号量
, 管程
, 消息传递
信号量
初始化
,P操作
和V操作
,这三种操作都是原子操作。P操作
(递减操作)可以用于阻塞一个进程,V操作
(增加操作)可以用于解除阻塞一个进程。管程
消息传递
参考: 进程同步的几种方式 - 静悟生慧 - 博客园
mutex
,
rwlock
,分为读锁和写锁。
Linux
的4种锁机制:
mutex
,
rwlock
,分为读锁和写锁。
spinlock
,
RCU
:即read-copy-update
.
update
成新的数据。RCU
时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。A* a = new A; a->i = 10
;在内核中的内存分配上发生了什么?程序内存管理:
A* a = new A; a->i = 10
:
A *a
:a
是一个局部变量,类型为指针,故而操作系统在程序栈区开辟4/8字节
的空间(0x000m),分配给指针a
。new A
:通过new
动态的在堆区申请类A
大小的空间(0x000n)。a = new A
:将指针a
的内存区域填入栈中类A
申请到的地址的地址。即*(0x000m)=0x000n
。a->i
:先找到指针a
的地址0x000m
,通过a
的值0x000n
和i
在类a
中偏移offset
,得到a->i
的地址0x000n + offset
,进行*(0x000n + offset) = 10
的赋值操作,即内存0x000n + offset
的值是10
。static
修饰符: 参考说一下static关键字的作用
static
修饰成员变量
static
修饰成员函数
Static
修饰的成员函数,在代码区分配内存。
C++
继承和虚函数
C++
多态分为静态多态和动态多态。
动态多态实现有几个条件:
实现过程
C++
内部为每一个存在虚函数的类都维持一个虚函数表,该类的所有对象都保存着一个指向这张虚函数表的指针。virtual
修饰符
定义虚函数
C++
内部为每一个存在虚函数的类都维持一个虚函数表,虚函数表中每一项指向该虚函数的函数入口地址,该类的所有对象都保存着一个指向这张虚函数表的指针。虚函数表的地址在每个对象的首地址。虚继承
VS编译器
它会占用对象内存, 而sun
公司的编译器就不会, 它存储与虚函数表上.inode
?
inode
,中文译名为"索引节点"。inode
,里面包含了与该文件有关的一些信息。Linux
引入了软链接和硬链接。Linux
解决文件共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。inode
号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名,使用ln
创建。inode
,但是其数据块内容比较特殊。// 判断系统时大端还是小段, 通过联合体, 因为联合体的所有成员都是从低地址开始存放的(也就是小端)
int fun1(){
union test{
int i;
char c;
}
test t;
t.i=1;
// 如果为大端, 则t.c为0x00 返回0; 如果时小端, 则t.c为0x01, 返回1
return t.c==1;
}
数据段
和bss段
,C语言
中其在代码执行之前初始化,属于编译期初始化。C++
中由于引入对象,对象生成必须调用构造函数,因此C++
规定全局或局部静态对象当且仅当对象首次调用时进行构造(inter x86
体系中内核态位于R0
, 用户态位于R3)
。server
,使得能够接收多个客户端的请求io复用
select
, epoll
这样的技术socket
线程?ps
命令查看cpu
,并且开了抢占可以造成这种情况。(???)schedule
或进程阻塞(此也会导致调用schedule)
时,还是会发生进程调度的。而单核情况下自旋锁其实是空的,啥都没干 !参考:
linux
上的自旋锁有三种实现:
cpu
,不可抢占内核中,自旋锁为空操作(就是什么也不做)。cpu
,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。cpu
,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。关于抢占式内核
与非抢占式内核
:
cpu
(即主动调用schedule或内核中的任务阻塞——这同样也会导致调用schedule)cpu
(即主动调用schedule或内核中的任务阻塞——这同样也会导致调用schedule)禁止内核抢占只是关闭“可抢占标志”,而不是禁止进程切换。显式使用schedule
或进程阻塞(此也会导致调用schedule)
时,还是会发生进程调度的。, 这就是同时又两个进程获得了资源所的原因 !!!
死锁
第一种死锁情况: 死锁发生在多核的情况
A
,B
两个CPU
。
A
上正在运行的a进程
已获得自旋锁,并在临界区运行。B
上正在运行的b进程
企图获得自旋锁,但由于自旋锁已被占用,于是b进程
在B CPU
上“自旋”空转。A
上的a进程
因程序阻塞,而被休眠。接着A
会切换运行另一进程c
。进程c
也企图获取自旋锁,c进程同样会因为锁已被占用,而在A
上“自旋”空转。A
上的a进程
与c进程
就形成了死锁。a进程
需要被c进程
占用的CPU
,c进程
需要被a进程
占用的锁。单cpu内核
上不会出现上述情况,因为单cpu
上的自旋锁实际没有“自旋功能”。第二种死锁情况
第三种死锁情况
spin_lock
比spin_lock_irq
速度快,但是它并不是任何情况下都是安全的。进程A
中调用了spin_lock(&lock)
然后进入临界区,此时来了一个中断(interrupt)
,该中断也运行在和进程A
相同的CPU
上,并且在该中断处理程序中恰巧也会spin_lock(&lock)
试图获取同一个锁。由于是在同一个CPU
上被中断,进程A
会被设置为TASK_INTERRUPT
状态,中断处理程序无法获得锁,会不停的忙等,由于进程A
被设置为中断状态,schedule()
进程调度就无法再调度进程A
运行,这样就导致了死锁!CPU
上就不会触发死锁。 因为在不同的CPU
上出现中断不会导致进程A
的状态被设为TASK_INTERRUPT
,只是换出。当中断处理程序忙等被换出后,进程A
还是有机会获得CPU
,执行并退出临界区。所以在使用spin_lock
时要明确知道该锁不会在中断处理程序中使用。自旋锁有几个重要的特性:
linux
在设计可抢占式系统的自旋锁时只是把自旋锁设计为"只是禁止内核抢占",而没有自旋(所以使用自旋锁的代码一定要可以很快执行完,否则进程就一直持着锁不释放,也不可被抢占).windows
消息机制知道吗,请说一说
Windows
是事件驱动的。事件驱动围绕着消息的产生与处理展开,事件驱动是靠消息循环机制来实现的。- 也可以理解为消息是一种报告有关事件发生的通知。
消息的概念和表示
消息(Message)
指的就是Windows 操作系统
发给应用程序的一个通告
,它告诉应用程序某个特定的事件发生了。
MSG[1]
用于表示消息,MSG
具有如下定义形式:typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
}MSG;
hwnd
是窗口的句柄,这个参数将决定由哪个窗口过程函数对消息进行处理;message
是一个消息常量,用来表示消息的类型;wParam
和lParam
都是32 位
的附加信息,具体表示什么内容,要视消息的类型而定;time
是消息发送的时间;pt
是消息发送时鼠标所在的位置。Windows
编程原理
Windows
是一消息(Message
)驱动式系统,Windows
消息提供了应用程序与应用程序之间、应用程序与Windows
系统之间进行通讯的手段。Windows
系统中有两种消息队列,一种是系统消息队列,另一种是应用程序消息队列。Windows
监控,当一个事件发生时,Windows
先将输入的消息放入系统消息队列中,然后再将输入的消息拷贝到相应的应用程序队列中,应用程序中的消息循环从它的消息队列中检索每一个消息并发送给相应的窗口函数中。Windows
程序的执行顺序将取决于事件的发生顺序,具有不可预知性。箭头1
说明操作系统能够操纵输入输出设备,例如让打印机打印;箭头2
说明操作系统能够感知输入输出设备的状态变化,如鼠标单击,按键按下等,这就是操作系统和计算机硬件之间的交互关系,应用程序开发者并不需要知道他们之间是如何做到的,我们需要了解的操作系统与应用程序之间如何交互。箭头3
是应用程序通知操作系统执行某个具体的操作,这是通过调用操作系统的API
来实现的;操作系统能够感知硬件的状态变化,但是并不决定如何处理,而是把这种变化转交给应用程序,由应用程序决定如何处理,箭头4
说明了这种转交情况,操作系统通过把每个事件都包装成一个称为消息结构体MSG
来实现这个过程,也就是消息响应,要理解消息响应,首先需要了解消息的概念和表示。Windows
消息循环
消息循环
是Windows
应用程序存在的根本,应用程序通过消息循环获取各种消息,并通过相应的窗口过程函数,对消息加以处理;正是这个消息循环使得一个应用程序能够响应外部的各种事件,所以消息循环
往往是一个Windows 应用程序
的核心部分
。windows
消息是windows
系统发送给windows
应用程序的一个通告, 告知特定的事件发生了, 例如鼠标单击,windows
应用程序再调用相应的窗口函数完成对消息的处理windows
系统是事件驱动的, 他的事件驱动围绕着消息的产生和处理展开的, 应用程序功能由windows
消息来触发,并靠的消息的响应和处理来实现. windows
消息提供了应用程序和应用程序之间, 操作系统和应用程序之间的通信.windows
系统维护了两个消息队列, 系统消息队列和应用程序消息队列windows
系统负责监控所有的输入设备,当输入设备输入消息时, 系统负责将消息放入系统消息队列,然后复制到应用程序消息队列中, 应用程序中的消息循环负责遍历所有的应用程序消息队列中的所有消息,并发送给对应的窗口函数完成处理.windows
应用程序的根本, 应用程序通过消息循环获取消息,并调用相应的窗口函数实现消息的处理, 正是消息循环使得应用程序能够及时的响应各种操作, 所以一个应用程序的核心往往是消息循环.概念:
内存溢出原因:
BUG
内存泄漏 ->
什么是memory leak
,也就是内存泄漏
进程和线程的区别: ->
进程与线程的概念,以及为什么要有进程线程,其中有什么区别,他们各自又是怎么同步的
常用线程模型
Future
模型
- 核心思想是异步调用
- 传统的同步方法,当客户端发出
call请求
,需要很长的一段时间才会返回,客户端一直在等待直到数据返回随后再进行其他任务的处理。Future
模型虽然call
本身任然需要一段很长时间处理程序。但是服务程序并不等数据处理完成便立即返回客户端一个伪造的数据(如:商品的订单,而不是商品本身),实现了Future模式的客户端在得到这个返回结果后并不急于对其进行处理而是调用其他业务逻辑,充分利用等待时间,这就是Future模式
的核心所在。在完成其他业务处理后,最后再使用返回比较慢的Future数据
。这样在整个调用中就不存在无谓的等待,充分利用所有的时间片,从而提高了系统响应速度。
Callable
接口配合使用。Future
是把结果放在将来获取,当前主线程并不急于获取处理结果。Callable
是类似于Runnable
的接口,其中call
方法类似于run
方法,所不同的是run
方法不能抛出受检异常没有返回值,而call
方法则可以抛出受检异常并可设置返回值。两者的方法体都是线程执行体。fork & join
模型
actor
模型 每个人都有明确分工,这就是Actor模式。每个线程都是一个Actor,这些Actor不共享任何内存,所有的数据都是通过消息传递的方式进行的。
actor
模型属于一种基于消息传递机制并行任务处理思想,actor
之间不共享任何内存它, 以消息的形式来进行线程间数据传输,进而避免了数据同步错误的隐患。actor
在接受到消息之后可以自己进行处理,也可以继续传递(分发)给其它actor
进行处理。actor
模型的时候需要使用第三方Akka
提供的框架。生产者消费者模型
一个/多个
线程来生产任务,然后再开启一个/多个
线程来从缓存中取出任务进行处理。master-worker
模型
master-worker
模型类似于任务分发策略,开启一个master线程
接收任务,然后在master
中根据任务的具体情况进行分发给其它worker
子线程,然后由子线程处理任务。worker
处理结束之后把处理结果返回给master
。概念:
Coroutine
。def A() :
print '1'
print '2'
print '3'
def B() :
print 'x'
print 'y'
print 'z'
12x3yz
。在执行A
的过程中,可以随时中断,去执行B
,B
也可能在执行过程中中断再去执行A
。协程和线程区别: ->
进程、线程和协程的理解
其他
多核CPU
呢——多进程
+协程
,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。Python
对协程的支持还非常有限,用在generator
中的yield
可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。open
和write
都是系统调用。如下:#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]){
if (argc<2) return 0;
//用读写追加方式打开一个已经存在的文件
int fd = open(argv[1], O_RDWR | O_APPEND);
if (fd == -1) {
printf("error is %s\n", strerror(errno));
} else {
//打印文件描述符号
printf("success fd = %d\n", fd);
char buf[100];
memset(buf, 0, sizeof(buf));
strcpy(buf, "hello world\n");
write(fd, buf, strlen(buf));
close(fd);
}
return 0;
}
write
,创建进程fork
,vfork
等都是系统调用。fork
调用示例fork实例
int main(void){
pid_t pid;
signal(SIGCHLD, SIG_IGN);
printf("before fork pid:%d\n", getpid());
int abc = 10;
pid = fork();
if (pid == -1) { //错误返回
perror("tile");
return -1;
}
if (pid > 0) { //父进程空间
abc++;
printf("parent:pid:%d \n", getpid());
printf("abc:%d \n", abc);
sleep(20);
} else if (pid == 0) { //子进程空间
abc++;
printf("child:%d,parent: %d\n", getpid(), getppid());
printf("abc:%d", abc);
}
printf("fork after...\n");
}
用户态切换到内核态的3种方式
Linux
的ine 80h
中断。CPU
在执行运行在用户态的程序时,发现了某些事件不可知的异常,这是会触发由当前运行进程切换到处理此异常的内核相关程序中,也就到了内核态,比如缺页异常。CPU
发出相应的中断信号,这时CPU
会暂停执行下一条将要执行的指令,转而去执行中断信号的处理程序,如果先执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了有用户态到内核态的切换。切换操作
ss0
及esp0
信息。ss0
和esp0
指向的内核栈将当前进程的cs
,eip
,eflags
,ss
, esp
信息保存起来,这个过程也完成了由用户栈找到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。cs
,eip
信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。预编译: 主要处理源代码文件中的以#
开头的预编译指令。
#define
,展开所有的宏定义。#if
、#endif
、#ifdef
、#elif
和#else
。#include
预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。//
和/**/
。#pragma
编译器指令,编译器需要用到他们,如:#pragma once
是为了防止有文件被重复引用。编译: 把预编译之后生成的xxx.i
或xxx.ii
文件,进行一系列词法分析
、语法分析
、语义分析
及优化
后,生成相应的汇编代码
文件。
汇编
xxx.o
(Windows下)、xxx.obj
(Linux下)。链接: 将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:
linux
内核。bug
经常会导致整个系统挂掉。QNX
,QNX
的文件系统是跑在用户态的进程,称为resmgr
的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了。正常进程
wait()
或者waitpid()
系统调用取得子进程的终止状态。unix
机制
unix
提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到:
wait / waitpid
来取时才释放。the process ID
the termination status of the process
the amount of CPU time taken by the process
等孤儿进程
init进程(进程号为1)
所收养,并由init进程
对它们完成状态收集工作。僵尸进程
wait
或waitpid
获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。exit()
之后,父进程没有来得及处理,这时用ps命令
就能看到子进程的状态是Z
。ps命令
就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。init
接管。init
将会以父进程的身份对僵尸状态的子进程进行处理。wait / waitpid
的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。kill
发送SIGTERM
或者SIGKILL
信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程
接管,init进程
会wait()
这些孤儿进程,释放它们占用的系统进程表中的资源SIGCHILD
信号,父进程处理SIGCHILD
信号。在信号处理函数中调用wait
进行处理僵尸进程。fork
两次,原理是将子进程成为孤儿进程,从而其的父进程变为init
进程,通过init
进程可以处理僵尸进程。
GDB
调试
GDB
是自由软件基金会(Free Software Foundation)的软件工具之一。GDB
的出现减轻了开发人员的负担,
break line-or-function if expr
。(gdb)break 666 if testsize==100
参考:
阻塞IO:
非阻塞IO:
IO事件
是否就绪。没有就绪就可以做其他事。信号驱动IO:
sigaction
函数对信号驱动IO安装一个信号处理函数, 继承继续执行, 当当IO事件
就绪,进程收到SIGIO
信号。linux
用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件
就绪,进程收到SIGIO
信号。异步IO:
linux
中可以调用aio_read
函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式
,然后立即返回IO复用/多路转接IO:
linux
用select/poll
函数实现IO复用模型
,这两个函数也会使进程阻塞,但是和阻塞IO
所不同的是这两个函数可以同时阻塞多个IO操作
。IO函数
进行检测。IO操作函数
(main loop)
或称事件循环(event loop)
事件监测
和事件处理
。CPU
可以访问内存的所有数据,包括外围设备 ,CPU
也可以将自己从一个程序切换到另一个程序。CPU
的能力被剥夺,CPU
资源可以被其他程序获取。page cache
,操作系统怎么设计的page cache
Page cache
是通过将磁盘中的数据缓存到内存中,从而减少磁盘I/O
操作,提高性能。inode文件节点
对应一个page cache
对象,一个page cache
对象包含多个物理page
。page cache
中有一部分磁盘文件的缓存,因为从磁盘中读取文件比较慢,page cache
中去查找,
Linux 内核
中,文件的每个数据块最多只能对应一个Page Cache
项,它通过两个数据结构来管理这些 Cache
项,
radix tree
: Radix tree
是一种搜索树,Linux
内核利用这个数据结构,快速查找脏的(dirty)
和回写的(writeback)
页面,得到其文件内偏移,从而对page cache
进行快速定位。Linux
内核为每一片物理内存区域(zone)
维护active_list
和inactive_list
两个双向链表,这两个list
主要用来实现物理内存的回收。这两个链表上除了文件Cache
之外,还包括其它匿名(Anonymous)
内存,如进程堆栈等。server
端监听端口,但还没有客户端连接进来,此时进程处于什么状态?阻塞io
则处于阻塞状态select, poll, epoll
方式的io复用
则是循环等待状态n
个线程,并让其运行起来,加锁去队列取任务运行Linux
下怎么得到一个文件的100
到200
行sed -n '100,200p' inputfile
head -200 inputfile|tail -100
awk 'NR>=100&&NR<=200{print}' inputfile
awk
的使用还是比较简单的参考如下链接即可: Linux awk命令详解
作用:样式扫描和处理语言。它允许创建简短的程序,这些程序读取输入文件、为数据排序、处理数据、对输入执行计算以及生成报表,还有无数其他的功能。
使用方法: awk -F 'field-separator' '{pattern + action }' filename
-F
:后面跟着分隔符, 可以使用'[xxx]'
可以设置多种分割符pattern
: 支持正则表达式action
:BEGIN
, END
, print
, printf
以及类似c语言
的逻辑操作内置变量
ARGC
:命令行参数个数ARGV
:命令行参数排列ENVIRON
:支持队列中系统环境变量的使用FILENAME
:awk
浏览的文件名FNR
:浏览文件的记录数FS
:设置输入域分隔符,等价于命令行 -F选项NF
:浏览记录的域的个数NR
:已读的记录数OFS
:输出域分隔符ORS
:输出记录分隔符RS
:控制记录分隔符实例:
ls -l | awk ‘{print $5 “\t” $9}’
找到当前文件夹下所有的文件和子文件夹,并显示文件大小,并显示排序
ls -l | awk ‘BEGIN {COUNT = -1; print “BEGIN COUNT”}
{COUNT = COUNT + 1; print COUNT"\t"$5"\t"$9}
END {print "END, COUNT = "COUNT}’
找到当前文件夹下所有的子文件夹,并显示排序
ls -l | awk ‘BEGIN {print “BEGIN COUNT”} /4096/{print NR"\t"$5"\t"$9}
END {print “END”}’
/4096/ 正则匹配式子
print $NF
可以打印出一行中的最后一个字段,使用$(NF-1)则是打印倒数第二个字段,其他以此类推。linux
内核中的Timer
定时器机制Linux 2.6.16
之前,内核只支持低精度时钟,(RTC, HPET,PIT…)
,初始化当前系统时间。HZ
(系统定时器频率,节拍率)参数值,设置时钟事件设备,启动tick(节拍)
中断。HZ
表示1秒
内产生时钟硬件中断的个数,tick
就表示连续两个中断的间隔时间。tick
中断,触发时钟 中断处理函数,更新系统时钟,并检测timer wheel
,进行超时事件的处理。Linux 2.6.16
之前,内核软件定时器采用timer wheel
多级时间轮的实现机制,维护操作系统的所有定时事件。timer wheel
的触发是基于系统tick
周期性中断。linux
只能支持ms
级别的时钟,随着时钟源硬件设备的精度提高和软件高精度计时的需求,有了高精度时钟的内核设计。Linux 2.6.16
,内核支持了高精度的时钟,内核采用新的定时器hrtimer
,Linux 2.6.16
之前定时器逻辑区别:
hrtimer
采用红黑树进行高精度定时器的管理,而不是时间轮;tick
中断,而是基于事件触发。tick
,基于该tick
,内核会扫描timer wheel
处理超时事件,会更新jiffies
,wall time
(墙上时间,现实时间),process
的使用时间等等工作。tick
,新内核定时器框架采用了基于事件触发,而不是以前的周期性触发。新内核实现了hrtimer(high resolution timer)
:于事件触发。hrtimer
的工作原理:
Timer
的时间 ,时钟到期后从红黑树中得到下一个 Timer
的到期时间,并设置硬件,如此循环反复。tick
中断,以便刷新内核的一些任务。hrtimer
是基于事件的,不会周期性出发tick
中断,所以为了实现周期性的tick中断(dynamic tick)
:
tick
时钟的特殊hrtimer
,
tick
时长,在超时回来后,完成对应的工作,tick
的超时时间,以此达到周期性tick中断
的需求。dynamic tick
,是为了能够在使用高精度时钟的同时节约能源,这样会产生tickless
情况下,会跳过一些tick
。时钟源设备(closk source device)
:抽象那些能够提供计时功能的系统硬件,比如 RTC(Real Time Clock)
、TSC(Time Stamp Counter)
,HPET
,ACPI
PM-Timer
,PIT
等。不同时钟源提供的精度不一样,现在PC
大都是支持高精度模式(high-resolution mode)
也支持低精度模式(low-resolution mode)
。时钟事件设备(clock event device)
:系统中可以触发 one-shot(单次)
或者周期性中断的设备都可以作为时钟事件设备。timer wheel
和 hrtimer
两套timer
的实现,内核启动后会进行从低精度模式到高精度时钟模式的切换,hrtimer
模拟的tick
中断将驱动传统的低精度定时器系统(基于时间轮)和内核进程调度。