探索C++底層機制-第一天

C++ 引用的本質是什麼?

①C++中的引用本質上是 一種被限制的指針(類似於線性表和棧,棧是被限制的線性表,底層實現相同,只不過邏輯上的用法不同而已)。

②由於引用是被限制的指針,所以引用是佔據內存的。

③在使用高級語言的層面上,是沒有提供訪問引用的方法的。並且引用創建時必需初始化,創建後還不能修改。

gdb 的原理

gdb 主要通過系統調用 ptrace 實現. ptrace 可以將 gdb attach 到指定線程, 這有兩種方式:

在 gdb 中運行一個進程, 再對其進行調試. 首先利用 fork 創建該進程, 再在子進程中調用 ptrace,

並將第一個參數設為 PTRACE_TRACEME, 表示此進程將被父進程跟蹤, 然後執行 exec 函數.

將 gdb attach 到一個已存在的進程. 指定進程的 pid, gdb 調用 ptrace, 將第一個參數設為 PTRACE_ATTACH,

將 pid 作為函數參數. 這樣, gdb 就成為該進程的父進程, 並跟蹤該進程.

通過這兩種方式, gdb 就和被調試進程建立了聯繫, 即成為其父進程, 該進程被父進程跟蹤.

此時任何傳遞給該進程的信號(除了SIGKILL)都將通過 wait 方法阻塞該進程, 並將信號轉交給父進程.

並且, 該進程如果調用 exec 函數, 都會接收到一個 SIGTRAP 信號, 使得父進程(gdb)可以在被跟蹤進程執行第一條指令前就可以做一些需要的工作.

這也是第一種實現方式的原理.

斷點*是通過內核信號實現的. 增加斷點時, 實際是將指定位置寫入指令 INT 3. 運行到此指令時,

會觸發 SIGTRAP 信號, 從而被跟蹤進程會被暫停, gdb可以捕獲到此信號. gdb將斷點組織為一個鏈表,

此時就可以查詢此鏈表, 檢查是否有匹配的斷點記錄, 當存在時就發生斷點命中, 就允許用戶做一些調試操作.

否則繼續執行命令.


編譯器會為const引用創建臨時變量

將常引用綁定到臨時數據時,


const int &A;

==編譯器會為臨時數據創建一個新的、無名的臨時變量,並將臨時數據放入臨時變量中,然後再將引用綁定到臨時變量。==臨時變量也是變量,所有的變量都會被分配內存。


常引用和普通引用不一樣。


因為臨時數據無法尋址,不能寫入。而引用是綁定到一份數據時,就可以通過引用對數據進行讀取和修改。即使為臨時數據創建一個臨時變量的時候,修改的也是臨時變量的數據,而不是源數據。這樣引用所綁定的數據和源數據不能同步更新,失去了操作數據的作用。


以代碼為例


void swap(int &r1, int &r2){

int temp = r1;

r1 = r2;

r2 = temp;

}

如果編譯器為r1和r2創建了臨時變量,那麼r1和r2的值怎樣都不會發生交換。


常引用只能通過const引用讀取數據的值,而不能修改數據的值。所以不用考慮數據更新的問題,也不會產生兩份數據。


以代碼為例


//該函數用來判斷數字是否為奇數

bool isOdd(const int &n){ //改為常引用

if(n/2 == 0){

return false;

}else{

return true;

}

}


int main(){

int a = 100;//

isOdd(a); //正確

isOdd(a + 9); //正確

isOdd(27); //正確

isOdd(23 + 55); //正確

}

對於第12行的代碼,編譯器不會創建臨時變量,引用n會直接綁定a,而對於13~15行代碼,編譯器會創建臨時變量來存儲臨時數據。即編譯器只有在必要的時候才會創建臨時變量。


IO複用的實現原理

IO設備的驅動程序中有一個等待隊列, 可以通過驅動程序提供的 poll 接口來獲取IO設備是否就緒, 也可以將進程加入到等待對應的等待隊列中.
當IO設備就緒時, 就可以通知等待隊列中的進程, 將其從睡眠中喚醒.
select, poll 的實現是:

掃描所有 fd, 利用 poll 接口檢測對應的IO是否就緒, 並將進程加入到等待隊列.

如果存在就緒的 fd, 就在遍歷後返回就緒 fd 的數量.

如果沒有就緒的 fd, 就睡眠一段時間.

如果休眠結束或被IO驅動程序喚醒, 繼續循環上面的過程. 如果是後者, 就會檢測到就緒的 fd, 從而可以在第二步返回.

epoll 流程也類似上面, 只有下面幾點不同:

epoll 在創建句柄時將 fd 集合拷貝到內核, epoll_wait 時不需要再拷貝 fd 集合, 這樣對同一集合多次調用 epoll_wait 時就只需要拷貝一次.

epoll 的句柄通過一個紅黑樹來維護 fd 集合, 每次加入時會先在紅黑樹中查找是否已經保存了該 fd,


時間複雜度是 log(N)log(N).

epoll 第一次也要遍歷 fd, 並將進程加入到等待隊列, 但是同時為每個 fd 指定了回調函數, 當 fd 就緒時,
設備驅動程序會喚醒進程, 並調用此回調函數. 這個回調函數的作用是將這個就緒的 fd 加入到就緒鏈表.
因此, 當進程從等待中被喚醒時, 就可以直接通過檢查這個就緒鏈表是否為空來判斷是否有 fd 就緒,
而不需要像 select/poll 一樣再次遍歷 fd 集合.

調用 epoll_wait 時, 會把就緒的 fd 拷貝到用戶態內存, 然後清空就緒鏈表, 最後再檢查這些 fd.
在水平觸發模式下, 如果檢查到這些 fd 上還有未處理的事件, 會將這些 fd 放回就緒鏈表中, 保證事件得到正確處理.

對於 fd 集合大小的限制, epoll 是進程可以打開的最大文件數目, 這個值保存在 /proc/sys/fs/file-max.


malloc的原理,另外brk系統調用和mmap系統調用的作用分別是什麼?

參考回答:
Malloc函數用於動態分配內存。為了減少內存碎片和系統調用的開銷,malloc其採用內存池的方式,先申請大塊內存作為堆區,然後將堆區分為多個內存塊,以塊作為內存管理的基本單位。當用戶申請內存時,直接從堆區分配一塊合適的空閒塊。Malloc採用隱式鏈表結構將堆區分成連續的、大小不一的塊,包含已分配塊和未分配塊;同時malloc採用顯示鏈表結構來管理所有的空閒塊,即使用一個雙向鏈表將空閒塊連接起來,每一個空閒塊記錄了一個連續的、未分配的地址。


當進行內存分配時,Malloc會通過隱式鏈表遍歷所有的空閒塊,選擇滿足要求的塊進行分配;當進行內存合併時,malloc採用邊界標記法,根據每個塊的前後塊是否已經分配來決定是否進行塊合併。

Malloc在申請內存時,一般會通過brk或者mmap系統調用進行申請。其中當申請內存小於128K時,會使用系統函數brk在堆區中分配;而當申請內存大於128K時,會使用系統函數mmap在映射區分配。

請你說一說C++的內存管理是怎樣的?

參考回答:
在C++中,虛擬內存分為代碼段、數據段、BSS段、堆區、文件映射區以及棧區六部分。
代碼段:包括只讀存儲區和文本區,其中只讀存儲區存儲字符串常量,文本區存儲程序的機器代碼。


分享到:


相關文章: