lambda表達式常見用法 :
【最常見】1 [capture list] {function body} // 自動推斷返回類型,并且無參數,
2 [capture list] (params list) {function body} // 自動推斷返回類型,并有參數
捕獲類型 :
值捕獲
int a = 123;
auto f = [a] { cout << a << endl; };
引用捕獲
int a = 123;
auto f = [&a] { cout << a << endl; };
隱式值捕獲
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕獲
隱式值捕獲
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕獲
int a = 123;
auto f = [&] { cout << a << endl; }; // 引用捕獲
1,不用宏,用 constexpr
2, 包括返回值,該加const,要加const
3,類的聲明,注意final,這樣就不用寫虛析構函數
4, 類默認值,注意用定義時初始化,比初始化列表更好
1,聲明: 函數聲明多用 inline __always_inline
2, 表達式: if判斷多用 likely unlikely
3, 類型: size_t
1,私有成員變量 加后劃線 私有成員函數,加前劃線, 這樣代碼一清二楚。 參考litao的類 class TransponderRPC
2, 返回值的場景,區分獲取成功和有值2種情況,用 std::pair<ResultCode, uint64_t> ,這樣返回值的意思清晰明確,調用者不容易用錯,并且方便
以后擴展。 參考函數 DefaultAddressRepository::GetPeerComponentId(uint64_t componentId)
3,整數,不要寫 uint32_t xx,要寫 auto xx = 0U
char* 定義,可以寫 static const auto objType = "obj"s;
迭代器(iterator)是一種智能指針,智能指針是一個類,具體來說是1個模板類
ARRAY是C++11提供的一種容器,VECTOR是STL提供的一種容器,區別在于前者不能擴容
弱符號:未初始化的全局變量是弱符號,或者__attribute__((weak)) 修飾的變量,函數是弱符號
效果: 同時存在強弱符號,則有限匹配強符號
【具體應用:庫中定義弱符號,使得可以被用戶定義的強符號替代。】
弱引用: attribute__((weak)) 或 attribute__((weakref)) 修飾函數聲明
效果: 如果未找到該符號定義,則不會報錯
【具體應用:將擴展功能的符號作為弱符號,定義放在so中,作為擴展模塊,如果去掉擴展模塊,也可以正常鏈接,如果不去掉,那么就自然加上了擴展模塊】
linux的程序格式:ELF格式。
elf頭包括魔數字,表明自己是ELF文件 (7f 45 4c 46),還有多少位,大小端,文件類型(可執行程序,共享SO文件,可重定位文件等)
還表明了 section 和 segment的偏移。
section是給鏈接視圖,指導鏈接器鏈接的,segemnt是運行視圖,是給鏈接器指導生成可執行程序內存布局的。
比如我們通常說的,代碼段,數據段, BSS 他們在section中也有這個概念,同時在segment也有這個概念。 除了這幾個 segemnt ,還有其他的某些segment,他們1個
segment會包含多個section。
ELF文件頭中,第一個字段是魔數,這個數字是用來表明自己是ELF文件(固定是7f 45 4c 46),ELF的字長,字節序,以及ELF版本
靜態鏈接:
靜態鏈接與虛擬地址 : 靜態鏈接就分配了虛擬地址,也就是確定各段的起始地址。
鏈接之后,怎么看分配到虛擬地址是什么 : 生成的elf文件,用objdump -h 看,可以看到鏈接后,就已經分配了虛擬地址()。由于ELF可執行可執行文件默認從0x8048000開始的,所以可以看到代碼段的虛擬地址不是從0x0開始,而是從0x84000往后加一點開始的。
比如通過 objdump -d a.out,查看了funcli的地址:
0000000000400677 <_Z5func1i>:
400677: 55 push %rbp
400678: 48 89 e5 mov %rsp,%rbp
40067b: 48 83 ec 10 sub 0x4007ae,%edi
40068c: b8 00 00 00 00 mov $0x0,%eax
400691: e8 aa fe ff ff callq 400540 printf@plt
運行a.out,gdb上去看這個地址是不是確實這樣的,而可以看到是這樣的 :
Breakpoint 1, 0x000000000040067b in func1(int) ()
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040067b <func1(int)+4>
breakpoint already hit 1 time
(gdb) bt
0 0x000000000040067b in func1(int) ()
1 0x00000000004006c7 in main ()
(gdb) disassemble
Dump of assembler code for function _Z5func1i:
0x0000000000400677 <+0>: push %rbp
0x0000000000400678 <+1>: mov %rsp,%rbp
=> 0x000000000040067b <+4>: sub 0x4007ae,%edi
0x000000000040068c <+21>: mov $0x0,%eax
0x0000000000400691 <+26>: callq 0x400540 printf@plt
0x0000000000400696 <+31>: nop
0x0000000000400697 <+32>: leaveq
鏈接中的重定位:重定位就發生在靜態鏈接中,可以說是靜態鏈接的核心。由于各段已經確定了起始偏移地址,所以各符號加上起始偏移地址就可以了。這里的關鍵是,如何知道哪些指令的地址要被調整呢,
就是記錄在重定位表中的額。
中斷:
linux內核的要求 :
1,保存上下文并恢復上下文
2,能夠將中斷處理分為緊急和不緊急的部分
3,能夠在處理中斷中,允許另一個中斷到來
4,能夠在處理中斷的特殊臨界區中,禁止中斷
x86 :
1, 中斷(cpu外部產生)和異常(CPU內部產生,段錯誤就是一種異常)
2,中斷又分為可屏蔽中斷(I/O設備產生的)和不可屏蔽中斷(電源,總線,內存等)。
3,中斷和異常都是由0~255之間的一個數來標識,該數字也叫向量(vector)。 其中不可屏蔽中斷和異常的向量是固定的,可屏蔽中斷的向量是通過中斷控制器的編程來控制的。
256個向量的分配如下 :
從0~31的向量對應于異常和非屏蔽中斷
從32~47的向量(即由I/O設備引起的中斷)分配給屏蔽中斷
剩余的從48~255的向量用來標識軟中斷。Linux只用了其中的一個(即128或0x80向量)用來實現系統調用。
中斷向量的值,就是用于在中斷向量表的偏移。
中斷向量從哪里獲取
外部中斷的是到時要從中斷控制器獲得,內部CPU產生的,直接內部就有了,不需要再去獲取,軟中斷的話就是指令里面直接自帶的。
為什么要圍繞中斷向量設計,包括填寫中斷向量表:
因為CPU設計成拿到向量,就會自動去找向量表,比較表中的權限,以及拿出處理程序的地址,去執行后續的動作。
中斷向量表的位置到底是不是從0開始 - 2個階段:
實地址模式下,內存的0開始的1K字節作為中斷向量表。
保護模式下, 在保護模式下,中斷描述符表在內存的位置不再限于從地址0開始的地方,而是可以放在內存的任何地方。為此,CPU中增設了一個中斷描述符表寄存器IDTR。
中斷向量表 :
存放了處理函數的地址信息;描述了進入后的特權級,CPU會自動與當前特權級比較,不同則會引起堆棧的更換,比如從用戶堆棧切換到內核堆棧;描述了是否要關閉中斷(會屏蔽可屏蔽中斷)
上半部與下半部的設計到底為了啥
為了不會長時間關中斷,導致丟中斷。
ARM上的NMI:
ARM是類似的,ARM上有IRQ和FIQ,其中FIQ就相當于X86上的NMI,即不可屏蔽中斷。而IRQ的話每次一旦進去,就硬件自動關閉中斷;但是ARM更加靈活,在某些架構上,是可以通過設置CPSR寄存器屏蔽的。
從中斷屏蔽 --到---> 中斷上半部下半部:
硬件產生的中斷可被稱為硬中斷(hardirp),執行中斷指令(int)產生的中斷為軟中斷。
Linux下硬中斷硬件上設計為可以嵌套的,但硬件設計是硬中斷也是設置為可屏蔽的(比如x86中的IF位),就是可以關閉嵌套,而一般是設計為一旦進入硬件中斷 ,就開啟屏蔽,不允許嵌套!
為什么這樣設計呢,因為這樣嵌套可能導致棧溢出。所以LINUX內核設計為不可嵌套。
所以要執行的盡量的快,不然長時間關中斷,會導致丟中斷(并不是說一關就會丟,雖然有一定緩存,但是是有上限的)。所以就引入了上半部下半部的概念。
軟中斷,是下半部的一種,還有tasklet和工作隊列。
【軟中斷】
【軟中斷 不等于 硬中斷下半部,另外還有tasklet和工作隊列,都是下半部的形式】
【軟中斷還有種很重要的用法,系統調用】
【軟中斷不能嵌套,不能屏蔽(會打斷硬中斷?軟中斷?),但是可以在不同處理器上同時執行,類型相同也可以】
說明了如果有其他軟中斷觸發,執行到此處由于先前的軟中斷已經在處理,則其他軟中斷會返回。所以,軟中斷不能被另外一個軟中斷搶占!唯一可以搶占軟中斷的是中斷處理程序,所以軟中斷允許響應中斷。雖然不能在本處理器上搶占,但是其他的軟中斷甚至同類型可以再其他處理器上同時執行。由于這點,所以對臨界區需要加鎖保護。
軟中斷留給對時間要求最嚴格的下半部使用。目前只有網絡,內核定時器和 tasklet 建立在軟中斷上。
cmake
cmakelist.txt
1, 繼承父目錄該文件的一切變量
2,里面的執行是一條一條順序執行的
3,查找外部的頭文件和so,使用者需要 find_package(xx ,比如 find_package(msgc, 這樣camke就會自動去對應指定目錄找并執行 Findxx.cmake 。 Findxx.cmake 是模塊提供方創建的,寫法就是把頭
文件的路徑,查找好并設置成名為 xx_INCLUDE_DIR 或者 xx_INCLUDE_DIRS 的變量,把so的路徑查找好并設置為 LIBXML2_LIBRARIES 方便使用者用這個變量找到: 比如下列寫法 :
一些語法:
add_library(vos-ext-objects OBJECT ./vos_move2ext.cpp) 并不是生成一個 vos-ext-object.so ,而只是生成1個打包,因為后面指定的是 OBJECT
LINK_LIBRARIES -(添加需要鏈接的庫文件路徑,注意這里是全路徑) 對比 TARGET_LINK_LIBRARIES - (這個是設置要鏈接的庫文件的名稱)
高級用法(變量范圍)
1,父目錄的變量自動繼承到子目錄
2,cache變量(set時指定cache)的范圍時整個工程
3, include和find_package (類似于引用頭文件,自動在使用include與find_pacpage的本文件繼承頭文件中的變量,并會傳遞這個include與find_package后面的子目錄(find_package沒驗證,應該也是一樣的)):
include(Findtest) # 從 CMAKE_MODULE_PATH 包含的路徑中搜索 Findtest.cmake 文件 (前面還有一句 : list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules))
類似的, find_package 和 include類似,也會自動繼承找到的Findxx.cmake中的變量。
內核源碼目錄:
init : 內核的初始化代碼
fs : 虛擬文件系統((VFS))的代碼
【kernel】 : 內核中最核心的部分,包括進程的調度(sched.c) ,以及進程的創建和撤銷(fork.c和exit.c);平臺相關的另外一部分核心代碼再 arch//kernel目錄下。
【mm】 : 與體系無關的內存管理代碼,與體系有關的內存管理代碼位于arch//mm下
arch:是architecture的縮寫。所有與體系相關的代碼都在這個目錄
【drivers】: 驅動代碼。這個目錄是內核中最龐大的一個目錄,顯卡,網卡,各種總線等等的驅動程序都可以在這里找到
【firmware】 :注意不是防火墻,而是估計讓計算機讀取和理解從設備發來的信號的代碼。舉例來說,一個攝像頭管理它自己的硬件,但計算機必須了解攝像頭給計算機發送的信號。
固件和驅動的區別在于,固件不是運行在OS運行的處理器上,而是運行在外設中的處理器上。
【block】 : 塊設備驅動,還有一部分位于drivers目錄
【lib】 : 此目錄包含了核心的庫代碼,實現了一個標準C庫的通用子集,包括字符串和內存操作的函數,以及sprintf和atoi系列函數。
【ipc】: IPC(進程間通信)。它包含了共享內存,信號量以及其他形式的IPC代碼
srcutiry: 包含了不同的linux安全
crypto: 內核本身所用的加密API,實現了常用的加密和散列算法,還有一些壓縮衣和CRC校驗算法。
select的內核實現 (參考自http://gityuan.com/2019/01/05/linux-poll-select/) 寫的好,可以多看看他的博客 )
核心處理 : 將自身線程加入文件描述符的等待隊列; 然后將線程休眠;等待文件描述符(可讀,可寫,異常)時,???將等待隊列的線程喚醒。
select中第一個參數的目的:
在*Nix系統中,文件描述符只是系統表的索引,而fd_set結構包含與這些索引對應的位掩碼。將描述符添加到fd_set時,將啟用相應的位。select()需要知道描述符的最高值,這樣它就可以循環遍歷這些位,并知道在哪個位停止。
在Windows上,套接字由內核對象的句柄表示,而不是由索引表示。fd_set結構包含一個套接字句柄數組和數組中套接字數量的計數器。這樣,select()就可以循環遍歷數組,這就是為什么在Windows上忽略select()的第一個參數的原因。
C++ 類幾個編譯器自動生成的函數,以及不自動生成的場景 :
(默認的移動構造干啥呢,它咋知道怎么移動呢,比如將原來傳入的臨時對象的某個值釋放什么的,答案是,默認的
移動構造僅僅調用所有對象的移動構造函數。。。)
構造函數并不都是,你定義1個,編譯器會把其他的都干掉,具體情況如下 :
3個構造函數之間的影響(重點是這4個構造函數間的互相影響的規則,因為實際的應用中,大部分都會定義構造函數):
定義無參構造,編譯器不會刪除默認的無參構造, 不會刪除默認的移動構造函數
定義拷貝構造,編譯器會刪除默認的無參構造,會刪除默認的移動構造函數
定義移動構造,編譯器會刪除默認的無參構造,會刪除默認的移動構造函數
【如果自己定義其它類型的構造函數,則上面所有的默認構造函數都不會生成!】
定義拷貝賦值或者移動賦值運算符,都會刪除上面的拷貝構造,應該還有移動構造(未驗證),但都不會刪除默認構造
智能指針:
了解下這個 https://cloud.tencent.com/developer/article/1688444
【【auto_ptr : 關鍵,不可共享且可轉移】】
【初始化要注意】
初始化不能用隱式轉換,目的是為了讓開發清晰的知道自己在處理智能指針 :
std::auto_ptr<ClassA> ptr1(new ClassA); // 正確
std::auto_ptr<ClassA> ptr1 = new ClassA; // 錯誤,編譯器不讓編譯過,因為開發可能沒意識到自己在處理的是智能指針
或者
std::auto_ptr<ClassA> ptr ;
ptr = std::auto_ptr<ClassA> (new ClassA); // 正確,寫的很長,明顯知道自己寫的是只能指針
ptr = new ClassA(); // 錯誤,編譯器不讓編譯過,因為開發可能沒意識到自己在處理的是智能指針
【智能指針間賦值操作符要注意 - 直接就發生所有權轉移了】
【判空要注意】
判空不能直接 if(ptr == nullptr) ,需要 if (ptr.get() == nullptr)
常見STL的一些底層實現 :
STL的迭代器是什么 - 不是指針,是類模板,具體用時,就是一個對象
vector :初始會有一片連續空間;擴容會擴大2倍,這會把原來的拷貝過來,再把原來的釋放。 看下這個 https://segmentfault.com/a/1190000040103598
vector迭代器失效的情況:
push_back ,導致失效是因為可能導致擴容
insert, 導致失效的原因是要移動后面的元素,以及可能擴容
erase, 導致后面的元素要移動
vector的remove:
1, remove不會刪除元素,不會導致size變小
2,remove的核心原理是: 每一位有3個步驟:1,檢查是否是否是remove的值,滿足則標記空洞,但并不會改值用下一個來填充(簡稱-記坑等后人)(為啥要交給后人來改,因為不一定有下一位,下一位也可能自身也是坑) ;
2,不是的話,就看前面是否有空洞,有則填充最前面的空洞(不一定是緊鄰的前一個空洞);填充則把填充的標記轉移到自身。 (簡稱-填坑要填最前面,填完自己也記坑 | 或者自己也是坑,記坑等后人)。
- 從上面第一點,如果是remove最后一位,則最后一位沒有變化,因為此時只是標記了空洞,下一位已經沒有了,無法在掃描下一位時,來填充這最后一位。
舉例 :
include <iostream>
include <vector>
include <algorithm>
using namespace std;
int main()
{
vector<int> vc;
for (int i = 0; i <= 9; ++i) {
vc.push_back(i);
}
vc[3] = vc[5] = vc[9] = 99;
for (auto itr : vc) {
cout << itr << endl;
}
cout << "after remove" << endl;
remove(vc.begin(), vc.end(), 99);
for (auto itr : vc) {
cout << itr << endl;
}
return 0;
}
輸出結果是:
0
1
2
99
4
99
6
7
8
99
after remove
0
1
2
4
6
7
8
7
8
99
map和set: 核心是紅黑樹(紅黑樹是一種更加平衡的二叉查找樹)
operator new 的好處 :
1,頻繁創建的對象,可以避免每次申請內存
2,即使是非頻繁創建,棧上構造對象,可以避免走入C庫的查找堆中可用空間的處理,速度更快
OS - 多核調度
2級調度的形式 - 先進入公共隊列,再根據每個CPU核的負載情況,分發到每個CPU核
負載追蹤 - 只采用2級隊列,會導致一直在某個CPU核,不均衡,所以需要收集每個CPU核的負載(歷史上每個任務的運行時間)
負載均衡的觸發 - 消耗低的CPU核心進行觸發,拉取其它CPU核心的任務到本地隊列
負載均衡的層級 - 先在低級觸發 (邏輯CPU域 -> 物理CPU域 ->NUMA域(多核CPU擁有本地內存)) - 減少跨級的高切換開銷
STL的容器:
分類:
容器的類型:
順序容器(vector, list, deque), 排列位置與其值無關。
關聯容器(map, set, multimap, multiset), 排列位置與其值有關。
容器適配器(stack, queue, priority_queue)
set : 當只是想知道一個值是否存在時,set是最有用的。
性能:
檢查為空:
使用empty()而不是size()來檢查容器是否為空,因為有些容器,比如list,它的size需要遍歷,而empty不需要
普通互斥鎖的替代方案 : 讀寫鎖 -> std::shared_mutex -> std::atomic
順序容器vector:通過 resize 提前預留好空間 -> 可以保證不變化 -> 可以不加鎖
關聯容器map:不要同時修改同一個key的value就寫
vector和list插入時,用emplace_back替代push_back, 前者只會使用構造函數,后者會使用構造函數和移動構造函數2個
插入:
map的插入,insert 相比 [] :
1,insert效率更高
2, insert更安全,如果[]賦值的一步失敗了,則會處于中間狀態 (賦值這一步干的啥 ? )
3,如果沒有默認構造函數,則只能用insert (試驗下 ? )
應用上的區別是,insert遇到相同key,則不會更新值,[]會。
時鐘周期是震蕩周期,是最基本的時間。
指令周期:執行一條指令需要劃分為3個階段:取指(去PC寄存器指向的地址種獲取指令地址并
拿到指令),譯指,運行指令,并將pc寄存器++;第一個和最后一個階段都需要若干個時鐘周期
綜合來說:1個指令周期 - 若干時鐘周期(取指) + 若干時鐘周期(執行指令)
前2個階段都是有控制器完成的,也就是CU,
而最后1個階段是由算術邏輯單元ALU完成的,也就是ALU。
調度機制,分為三個層級:
長期調度:比如一個程序嘗試運行,操作系統是否應該立即創建對應進程,并將進程狀態改為就緒狀態呢?
長期調度像閥門,用于限制系統中被短期調度管理的進程數量 -> 是為了減小短期調度的開銷
由于進程可以分為 計算密集型和I/O密集型,所以長期調度會根據CPU, I/O利用率情況,選取合適的計算密集型或I/O密集型進程,交給短期調度 -> 為了有效控制系統中的資源利用情況,避免出現
激烈的資源競爭或者某項資源利用率過低的情況
也就是長期調度,負責將進程狀態從新生->就緒
短期調度:負責進程在 就緒,運行,阻塞狀態間的切換。
中期調度 : 實際是換頁機制的一部分。 它是從內存的角度考慮,當系統中的內存不夠,將處理就緒或者阻塞這2中狀態的進程設置為掛起就緒狀態和掛起阻塞狀態,并將掛起進程的內存也換入磁盤。當內存夠時,又
被替換為就緒或者阻塞狀態。
DMA使用的2個主要限制 (為什么寫文件,寫日志沒用DMA):
1, 需要外設支持DMA協議
2,需要批量搬運數據,如果是少量,零碎的搬運,那么由于DMA協議,那么在總線上傳輸的數據更大了(DMA協議也會占用總線資源),這種cost可能導致更加劣化
3,(DMA搬運的數據不要求一定是連續的,這一點不是約束)
RDMA + CBDMA
LINUX內核的存儲軟件棧 (從上到下):
VFS 虛擬文件系統(包括頁緩存機制等)
多種文件系統(比如ext4文件系統, ext3)
I/O調度器 (可能存在多個對存儲設備的訪問請求,以一定的順序將請求發送給設備驅動)
設備驅動
tips : 上面的頁緩存和I/O調度器的策略是否有可以優化的部分 ?
文件數據:文件的內容
文件元數據:存儲文件數據的支撐性信息,包括文件模式,文件所有者,大小,文件訪問時間等。
inode : 1,存放文件元數據 2,存放指向存儲塊的多級指針。
inode和文件是一一對應的。那么有多少類型的文件呢 :
linux 支持的文件類型 :常規文件 目錄文件 符號鏈接文件 FIFO文件(管道) 套接字文件 字符設備文件 塊設備文件
注意,一個文件的文件名不是這個文件的元數據,并沒有存放在這個文件的inode中。 這引入了2種文件 :
而是存放在目錄文件這個文件的inode中。 目錄文件的核心就是 :存儲文件名和inode的映射關系。
從上面目錄就可以引入 硬鏈接 的概念 : 硬鏈接就是1個或2個目錄文件下,又增加了一個新的文件名和原始inode的映射關系。 也就是說有2個映射關系(2個文件名),同時對應1個inode。
軟鏈接 :和硬鏈接不同,它實在的增加了1個inode,也就是增加了1個文件,這個文件的內容就是原始文件的路徑。當然,這還在其所在的目錄,增加了1個映射()映射到這個新加的文件)
文件描述符: 該整數值是個索引值,是進程級文件描述符表的下標。該表中每一項本質是對inode的一層封裝(實際不是直接存儲inode,而是存了指向inode的指針,并且該指針是放在了全局的inode表,這里只是存儲了指向全局inode表的指針),
打開文件返回文件描述符的本質:就是針對inode,在數組中分配一個文件描述符結構,并把數組下標,也就是整數fd,返回給進程。
- 中間的全局文件描述符表,存儲了指向inode的指針,以及偏移值(【當使用dup,或dup2重定向時,2個進程級文件描述符表,也就是2個fd,都會指向同一個中間的全局文件描述符表項,由于指向同一個中間表項,
就可以共享同樣的偏移值】)。
【ARM總線】
按照總線在soc系統的位置:
AMBA總線:片內總線,片內總線負責連接cpu芯片內部的各個模塊。
pci/pcie總線:片間總線,負責連接外設。任何嘗試生產的設備只要符合PCI規范,都能通過PCI插槽和CPU通信
有些總線訪問使用自選鎖的目的是,總線必須先占有,再使用,而自選鎖來判斷占有,效率更高。
【ARM函數調用】
進入子函數:
1, 壓棧壓了啥:壓了 fp和lr。 fp是調試用的,用來推棧,有些編譯器可能不加。 lr如果函數不會更進一步調用,那么lr可能也不會壓棧。SP沒有壓棧的說法,隨著子函數進入和退出先減多少,再加多少就行。
2, 父函數做了啥 : bl指令,把返回地址放在lr寄存器中,注意,這里不是壓棧,只是讓到了lr寄存器中。(當然還有保存入參到三個寄存器中)
子函數做了啥 : 負責壓棧,負責壓棧,負責壓棧,就是上面說的fp和lr。只有子函數知道自己是否調用了更進一步的子函數,所以只要是否要保存lr,而且fp也是由子函數壓棧,理論上由父函數壓棧,只要約定好,都是可以的,但是ARM規定,如果fp壓棧,必須由子函數來做。 (當然還有去除r0,r1,r2, 就是去除參數)
3,哪些是必須要壓棧的:沒有哪個寄存器是必須壓棧的,fp是調試用,有些編譯器就是省略了這個,比如選項"-fomit-frame-pointer"還專門用來忽略fp; lr的話,要看被調用的函數A,是否進一步調用了函數B,如果沒調用,也不會對lr壓棧; pc和lr,編譯器不會壓棧,除非指定 -mapcs-frame
子函數退出:
1,先把壓棧的回復到寄存器,這里面就包括把lr寄存器恢復了。
2,執行指令 bl lr指定,跳回去。
ARM (壓入返回地址):
調用者:
bl max
被調用者:
push {r11, lr} /* 序幕開始:保存幀指針和返回地址到堆棧*/
x86 (壓入返回地址): 調用者call指令一條就自動完成了壓棧:
調用者:
call 80483ed <bar>
【伙伴系統(大于2個頁面)】
作用:避免外部碎片。核心優點:最大的特點是性能強大。2個伙伴塊,地址只有1位不同,且差的這一位從塊的大小可以算出。
2個內存塊要是伙伴系統的一定要滿足3個條件:地址連續,大小相同,是從同一個塊中出來的。
缺點: 一個很小的塊往往會阻礙一個大塊的合并;申請的頁不是2的冪次方,就會有浪費。
【slub分配器(小于2個頁面走這里)】
作用:處理很小的內存申請,避免內部碎片。它總的效果就是維護了一堆小內存的池子,申釋放都不會直接交給伙伴系統,而是把該大小內存放到池子中,以便以后申請再交還回去。
【SO的分布】
1,so的代碼段和數據段是自己一塊的。 2,so的堆,和整個進程的堆在一起的,so的棧,處于某個線程中。
【 缺頁異常的場景 】 1,虛擬內存和物理內存有映射關系了,但是物理內存也沒分配 - malloc 2,虛擬內存和物理內存都沒有映射關系,比如內存不夠操作系統的換頁到磁盤,以及MMAP。
string_view :
1,優勢,效率高: 它去掉前綴,后綴,或者算substr時,效率非常高: 內部只保存了字符起始和長度的位置,這幾個操作只是挪動了指針而已
2, 劣勢:1, 生命期易出錯:它只是挪動指針,數據本地還在原來的string,或者char *,要注意生命期要考慮數據本體
2,一定要注意 data() 函數,它不像string的data()會自動帶'\0'。 這完全看string_view 是誰的view,一般這個"誰"都是有結尾的"\0"的,包括常量字符串與string
可以轉化為string:
string_view a = "hello";
string b = {a.begin(), a.end()};
可以轉化為char * :data()
【maps文件】
從maps文件看,so被加進去后,自己的代碼段和數據段自己靠在一起,但是堆是放在公共[heap]區,棧是放在對應線程的棧區。棧區每個線程都有1個,最下面的[stack]只是默認線程的棧區。
換頁對象:代碼段, 堆區棧區, 文件buf。 它們的換出策略都不同。代碼段由于是只讀,所以直接擦除,堆區棧區是換到swap區(就是塊flash);文件buf,則回寫到flash中文件。三種都可以釋放物理內存。
注意: 上面說的堆區和棧區換出并不是所有linxu都支持,必須開啟swap機制才會換。其它兩項則都支持。
具體換哪一頁呢: LRU算法: 核心操作就是每次訪問某個物理頁,就把物理頁挪到隊列尾部,而需要換出時,就把隊列頭部的換出即可。它的原理是局部性原理。
【寫時拷貝(加載動態鏈接庫以及進程fork2種常見會用到)和內存去重特性(OS自身不斷掃描相同部分)】
【TLB】
TLB加速的是什么 - 加速的是多級頁表的查詢,只用查詢一次TLB緩存即可。
TLB緩存的是什么 - 緩存的是多級頁表的核心內容 - 虛擬頁號->物理頁號。 拿到物理頁號后,還是要加上頁內偏移(12位)
切換進程時,需要刷新切換TLB。
為啥不是切換線程時刷新呢 - 因為線程共享統一地址空間,所以同一個進程內不同線程,其虛擬物理頁號,對應的是同樣的物理頁號。
【啟示 - 所以雖然調度的單位是線程,但是同一個進程內的線程調度,少了TLB的刷新開銷,不同進程的線程調度,增加了TLB的刷新開銷。】
【大頁內存】
大頁的主要大小有2MB和1G。默認是4KB
大頁的優化點:常見的是減少TLB MISS,還有1個優化點是: 減少缺頁中斷(因為一次缺頁中斷會加入一個物理頁,現在一次性可以加入2M的物理頁,原來4KB的物理頁需要加載很多次才能把2M加完)。
大頁的優化代價 : 會造成一定的內存消耗,比如原來一個物理頁里面可能有2K是真正使用的,那么4KB的物理頁面的情況下,加載后浪費了2KB,如果改為2M,浪費了很多。
具體使用 - linux原生手段:掛載hugetlbfs特殊文件系統到某個目錄,然后通過mmap映射這個文件系統來使用共享內存, OS的mmap發現對端是這個,就可以對頁表定制化處理,進行大頁處理。
華為RTOS的話,是直接將exe目錄下的所有可執行文件加載時,都使用大頁。
【微架構級性能優化的cache - 有個印象就行】
1,TLB緩存 - 把虛擬地址和物理地址的映射,存放在TLB緩存里面,這樣就可以不走幾級頁表,一級級訪問來確認對應的物理地址。大小通常是1個頁表(4K)或以上。
【優化思路 - 把緊密相關的大塊處理,放在同樣的文件中,使得其代碼段相鄰】 手段 - 大頁機制,以及減少SO或者SO組合優化(SO加載到大頁位置;多個SO代碼組合到一起,以及消除SO跳轉)
2,cache miss - 程序的局部性原理,讀取指令時,那么認為隨后一段也可能會執行,所以把他們加入cache中。
【優化思路 - 把緊密相關的代碼行,放在臨近的代碼上,使得其代碼段相鄰,這樣也可以使得其可能加入cache】
3,分支預測 - CPU內部處理,臨近的匯編指令級別。CPU指令的流水線機制,使得自動把代碼段相鄰的下面幾條指令加入流水線,
幫助編譯器排布匯編指令 - if-else的likely和unlikely;函數重排(根據函數調用關系,將函數再代碼端根據調用棧順序進行重排);將多進程改為多線程(進程切換會導致TLB刷新 - )
perf的branch miss就是描述這個;
cache miss和TLB miss關系 : cache一般有l1 l2 l3,他們是接收物理地址作為輸入,然后在cache中找內容并返回。所以:先有TLB miss,才會有 cache miss。
(注意,有些CPU l1 cache是接收虛擬地址作為輸入的,這種情況下l1 cache的cache miss是發生在 TLB miss前)
管道 :
消息沒有類型是字節流;發送和接收端各1個進程
消息隊列 :
消息有類型,所以接收端可以選擇類型接受或者全接受; 發送和接收端可以同時有多個進程。
相同點 :
發送端寫滿了都會阻塞,接收端無數據了也會阻塞。 內核都為其在內部維護了一段緩沖區。
【硬中斷 https://www.itread01.com/content/1540961643.html 】
【x86】
NMI在x86構架上已經存在很長的時間(8086就已經有了)。一般的中斷INTR會受到IF的影響,當IF=1時,INTR被遮蔽,而NMI不受到IF設定的影響,CPU都會響應。
【ARM】
Arm處理器上有沒有NMI呢?這個需要分情況來說。眾所周知,現代的armCPU有Cortex-A/R/M序列處理器,Cortex-A為應用處理器,Cortex-R為實時處理器,Cortex-M為MCU處理器。
Cortex-M處理器有NMI。因為Cortex-M處理器用在MCU上,有的MCU用在非常關鍵的場景,比如火災監測,它需要不管當時MCU在處理什麼任務,
一旦監測到火災,必須立即報警和響應(噴水?)因此Cortex-M構架的CPU都有由NVIC支援的NMI,這個NMI是沒有任何辦法去mask的,即使在Cortex-M因為關鍵的錯誤進入了hard fault,甚至是lock up的狀態,NMI也可以被響應。不管怎麼設定PRIMASK,FAULTMASK都不能遮蔽NMI 。
Cortex-A和Cortex-R處理器沒有獨立的NMI。 在Cortex-R上對FIQ有特殊的處理,可以通過配置(非軟件手段,CFGNMFI輸入訊號)將FIQ變成真正意義上的NMI(在ARM官網也提到ARMV7部分實現可以這樣做,什么是ARMV7呢,cortex-A,cortex-R,cortex-M就是該架構的三種子實現)。
在Cortex-A和Cortex-R處理器中,有兩個中斷訊號,IRQ, FIQ, FIQ代表fast interrupt. FIQ在 arm的處理器裡有很長的歷史,早在armv4構架的arm7處理器已經存在,在armv8 64位構架之前還有與之對應的FIQ處理器模式。
【ARM處理器7種工作模式】
---------- 用戶模式 ------------
用戶模式 用戶模式不能切換到別的模式
------以下全是特權模式 (特權體現在:1,可以訪問ARM內部寄存器以及外設 2,特權模式可以切換到用戶模式,用戶模式無法切換到特權模式) --------
系統模式
---------如下5種又稱為異常模式 (異常模式和系統模式的區別不只是名稱上,核心在于如下幾種異常模式有些特殊的寄存器是自己模式私有)-----------
IRQ模式
FIQ模式
管理模式 CPU上電后默認模式 + 軟中斷
中止模式 訪問沒有權限的內存地址,就會進入該模式
未定義指令終止模式 軟件仿真是,如果
模式的更換 -
通過修改CPSR寄存器的[4:0]即可實現更改處理器模式
7種異常, 本質上7個異常向量地址 (對應上面5種異常模式):
Highest Reset
Data Abort (段錯誤或者非對齊訪問都是這里)
FIQ
IRQ
Prefetch Abort
Lowest Undefined instruction and SWI。
優先級體現在:1,同時發生高優先級先被響應(也是要注意中斷:中斷中的fiq和data abort同時發生,雖然會先響應data abort,但是會穿插著執行fiq,把fiq執行完,再接著執行data abort。搜索“arm exception priority”即可看到arm官網描述)
2,嵌套(但是注意中斷的情況有點特別,簡單說,所有的異常都會導致irq無法嵌套,但是fiq除了復位和fiq,其它都可以打斷: Disable interrupts. IRQs are disabled when any exception occurs. FIQs are disabled when an FIQ occurs and on reset. )
中斷上半部下半部:
最大的不同是,上半部不可中斷,下半部可以中斷。習慣性的放在上半部的是不可被其它中斷打斷的任務。上半部的執行上下文應該就是中斷的異常向量表走下來的上下文,由于
ARM關閉了中斷(軟件應該也要關閉中斷(否則如果當前是irq,可能還是會被fiq打斷));下半部的實現有,軟中斷,tasklet,工作隊列。前2者的相同點是不能調用可能睡眠阻塞的函數;工作隊列可以。
FIQ之所以fast體現在以下方面:
FIQ和IRQ同時發生時,FIQ有更高的優先順序,先被處理。
在處理IRQ時如果FIQ沒有被mask, FIQ可以搶佔IRQ異常處理過程。
在發生IRQ異常,做IRQ異常處理過程中,CPU HW自動mask IRQ, 但是不mask FIQ.
但是FIQ不是真正意義上的NMI,因為它可以被軟體mask。通過設定CPSR.FIQ bit就可以(引入安全擴充套件和虛擬化擴充套件之外情況有點複雜,暫時不表)。
注意,linux-arm的情況下,linux并沒有設置fiq
【區別ARM的軟中斷和LINUX的軟中斷】
swi :software irq,是ARM的一個軟件中斷指令,產生swi異常; - 我們常說的系統調用,通過軟中斷進入系統模式,就是指的這個。
softirq : linux kernel自己造的軟中斷,和硬件無關。 - 我們常說的中斷下半部的軟中斷,就是指的這個,而不是arm的軟中斷模式。
softirq也有優先級,但是同一個cpu上,softirq不會強占,優先級僅僅體現在同時發生時,先處理高優先級的。
我們這樣來理解為什么有softirq這個東西,什么時候有:
- softirq 的觸發時機:系統調用即將返回,硬件中斷即將處理完,那些被softirq的被標記的(通常是硬件中斷去設置的標記,因為常常這樣標記,就表明還有工作要做,這樣就是所謂的"下半部“),就會被處理。而
由于這里已經不再屏蔽硬件中斷,所以系統的響應性能提高了。 - 原文:Whenever a system call is about to return to userspace, or a hardware interrupt handler exits, any 'software interrupts' which are marked pending (usually by hardware interrupts) are run (kernel/softirq.c).
那為啥又有tasklets:
- 主要是因為3點softirq不具備的優點: 1, 本CPU不會被其它下半部機制搶占(softirq,tasklet,work queue),也就是不用擔心重入問題。 2,其它CPU不會運行同一個tasklets,不用擔心并發問題。 3,可以在運行時
動態指定新的tasklets(softirq編譯時寫好了就不能增加了)。
拷貝構造函數于賦值 (區別的核心不是有沒有=號,而是是否先創建了一個對象):
下面的例子中,都有=號,但是就不一樣 :
A a; 默認構造
A b; 默認構造
b = a; 賦值函數
A a; 默認函數
A b = a; 拷貝構造函數函數
c++string的實現 epoll 真正安全的單例 https://cloud.tencent.com/developer/article/1606879 ? 產品的cmake 工程
如何避免多余的復制,左值右值操作符
異常的研究: http://baiy.cn/doc/cpp/inside_exception.htm
字面值常量 ?
怎樣避免類被繼承
C++中 構造函數(constructor): X() 拷貝構造函數(copy constructor):X(const X&) 拷貝賦值操作符(copy assignment):operator=(const X&)
移動構造函數(move constructor):X(X&&) C++11以后提供 移動賦值操作符(move assignment):operator=(X&&) C++11以后提供 析構函數(destructor):~X()
有什么講究 ?
constexptr 函數
constexptr 函數常見用法 ,2個要求滿足即可,且相比inline函數,沒有空間消耗代價,更好:
1,返回類型,輸入參數類型是 int ,char 等字面值類型。
2, 輸入參數必須是常量,或者回溯調用棧,上層的上層的上層。。是常量
inline 函數
inline函數 :
原理:會擴大函數的大小,用空間換時間。
注意: 不能超過10行,不能包含while for ,switch等,編譯器會視為比較復雜,改為普通函數代替。
如何禁止1個類被繼承
移動構造函數,移動賦值函數是為了解決這種場景:
1,需要用1個對象a,初始化另1個對象b,或者直接用a給b賦值。
2,a在上面初始化完后,就立馬丟棄不用了 - 比如函數return a等。 這種對象都是臨時對象,我們給它一個定義,叫做“右值”。
( 那么上面的構造函數,或者賦值函數,就是要傳入1個馬上不用的對象,或者傳入1個“右值”,更進一步,傳入一個它的引用,就叫“右值引用”。)
所以我們希望這樣設計: 設置1類特殊構造函數和賦值函數,它直接把臨時對象的指針,或者資源改為自己的,能直接改,而不是復制,是由于這些資源來自臨時對象的,不用考慮隨后改為自己的,
臨時對象還有業務的作用,導致臨時對象用不了了。 作為收尾,要注意,析構函數要作區分,就是區分資源已經被改給別的對象了,常見的就是判斷指針為nullptr。
基于以上思路,所以我們約定,1,定義這類構造函數和賦值函數,他們的特點是有 && 。 2,析構函數作點區分處理。 3,隨后當return 臨時對象時,或手動調用std::move轉為右值引用的場景,典型是std::move并
給另一個對象賦值時,編譯器就可以調用這些構造函數。當然,你不定義的話,編譯器就沒犯法了。
所以 && ,也就是右值引用的核心是: 和函數的調用者形成一種約定,使得調用者知道調用該函數(移動構造函數,移動賦值函數 ,普通函數等)后,該變量不再有效,所以函數設計者指定入參/&& ,調用者就必須指定 std::move,引導調用者明白的意識
到,傳入后值就無效了,達到性能的提升。 注意,如果入參是右值引用,和普通引用一樣,都不會導致創建2個參數。
【特別典型的應用場景是】 (試驗時編譯參數是 g++ main.cpp -fno-elide-constructors ,這樣可以避免優化,看出效果),返回值是臨時的:這種需要:
1,定義移動構造和移動賦值函數 - 這樣會使得這個對象的構造更快速。 使用時,也需要使用std::move來標注希望調用移動構造/移動賦值。
2,普通函數入參直接定義右值引用 - 這樣某些情況下,可以避免對象內部的大量拷貝。 (注意并不是說入參的臨時對象會構造個對象,右值引用本身和左值引用一樣都是引用,入參不會有新對象了,
而是說,用了右值引用,里面可以放心把入參直接掏空,函數返回后,外界不會再用入參變量了)
3,函數返回值聲明為右值引用 objectType&& funcXX()。這樣外界還要結合調用移動構造函數或者移動運算符才有意義 - 優化的核心,還是在于移動構造函數或者移動預算符
虛函數不允許使用缺省參數
重載是一個類內部的,不涉及父類和子類, 父類和子類間的是隱藏和重寫(就是正經的多態特性),他們的區別如下:
【 子類是否有virtual不影響下面兩條結論 】
父類函數無virtual 函數名相同 (參數和返回值任意組和)參數相同/不同/返回類型相同/不同 - 父類的函數被隱藏了,父類函數是調用不到的
父類函數有virtual 函數名相同 參數相同 - 正經的覆蓋,即多態的特性,同時返回值必須相同,不然編譯不過。
參數不同 返回值相同/不同 - 父類的函數被隱藏了,父類函數是調用不到的
構造函數與異常機制:
構造函數與異常機制
構造函數中如果發生了異常,則不會調用析構函數。這種做法要注意,容易出現內存泄露問題。
析構函數與異常機制. 析構函數中一旦拋出異常,就會直接導致程序退出 【異常機制使得一旦捕獲不到,一層層往上退,但析構函數特殊,沒有上層,一旦往上退,就會就會退到上層,上層直接導致進程退出。】
所以有2個做法:
a, 不要在析構函數中寫try catch
b, 一定要寫,則catch一定要捕獲完成,不要捕獲不完整。
反之,構造函數中沒有返回值,可以用異常機制。
0611:
gic:
中斷,就是讓CPU停下來,響應CPU中斷引腳對應的外部設備的請求。最開始每個外設有1個CPU的中斷引腳,后面設備逐漸增多,但是CPU的中斷
引腳無法不斷增多,并且,中斷同時出現,還要響應更重要的。 于是ARM上引入了 GIC,即通用中斷控制器。
中斷的處理中,需要考慮的主要是:
送中斷信號:
CPU當前沒有處理中斷,GIC同時來了多個中斷,那么送哪個中斷信號到CPU中斷引腳呢:
送優先級最高的中斷
1,選擇最高優先級的中斷:
GIC輸入端有多個中斷信號,此時要決定送哪個到CPU中斷引腳,這時就涉及到中斷的優先級。
2,選擇好后,確定是否立刻送:
a,如果是正在處理的中斷,和要送的中斷,是同一個中斷,必須等到之前的中斷處理完,才會再處理這個中斷 (是丟棄了這個中斷嗎)
上半部: 中斷處理中,會線關閉中斷,執行完成后,再通知GIC打開中斷,且這個中斷已處理完畢,GIC就可以繼續響應該中斷(這之前GIC是直接丟了這個中斷,還是丟了所有中斷?)
上半部和下半部,我們平時注冊的中斷處理,屬于哪個,比如 request_irq()
0609:
分段與分頁是2種并列的機制,不是互相配合的機制。
虛擬內存的頁機制,怎么解決物理內存不足的問題:
1,換頁:比如物理內存低到一定閾值,就會觸發換頁,把最近不常用的頁換出,寫到磁盤。當然,后續再訪問時再把頁調進來。
2,按需分配:申請了內存(這里的本質是占用了虛擬內存,分配了虛擬頁) 不會立刻映射到物理內存,只有訪問時才映射。
物理內存分配:
伙伴系統分配器 :分配的最小單位是物理頁。
SLAB分配器:但是有很多申請的內存大小通常是機制或幾百個字節,遠小于1個物理頁,就出現了SLAB分配器。
碎片分為外部碎片和內部碎片。外部碎片是動態產生的,也就是有很多不相鄰的小內存塊,他們無法被分配;內部碎片只是內存塊內部占不滿,被分配出來。
0513:
vector的全部刪除- clear
1,clear只釋放每個元素,不釋放vector自身的空間,為了效率還會預留。 也就是說,clear后,size()會為0,但是 capacity() 不變。
// 那么自然情況下,這個vector什么時候釋放吶 ,是vector退出了其作用域時,比如一個函數里面有個vector,退出這個函數,那么這個vector的空間會釋放。
如果我要想提前釋放有辦法嗎,可以通過下面的swap函數來做 :
void clearVec(vector<A *>& vec)
{
vector<A *> tmp;
tmp.swap(vec);
}
2,上面clear說道會釋放每個元素,但是要注意,如果是指針,不會釋放,比如vector中放入的是 對象,clear時會釋放,但如果放入的是指向對象的指針,則不會釋放。
vector的單個或者范圍刪除 - erase
相比clear的不同點
1,erase可以清除一個或一段范圍的內容
2,會返回刪除元素的下一個元素的迭代器
相比clear的相同點:1,減少size,不會減少 capacity 。 2,如果不是指針,erase會釋放元素,如果是指針,不會釋放