一个C/C++协程库的思考与实现之协程栈的动态按需增长
https://github.com/DoasIsay/ToyCoroutine
如何檢測協(xié)程是否需要進行棧擴充?
我們先思考一個問題,glibc的pthread_create創(chuàng)建的線程是如何檢測到用戶棧的溢出而及時終止線程的?
如下代碼
g++ test.cpp -lpthread
strace ./a.out 結(jié)果如下圖
?
由strace 結(jié)果可知pthread_create先使用mmap為線程申請了的用戶空間stack,然后使用mprotect對stack的棧頂進行保護,即棧頂?shù)?kb無讀寫權(quán)限,一旦被讀寫就說明產(chǎn)生了棧溢出,操作系統(tǒng)就會向讀寫此4kb的任務(wù)發(fā)送SIGSEGV信號,最后才是調(diào)用clone創(chuàng)建線程
如果越過這4kb訪問前面的內(nèi)存會怎樣?如果剛好是另一個線程的stack?只要不訪問這4kb或其它線程的這4kb都不會有問題,頂多就是會破壞其它線程的棧,或自已的棧被其它線程破壞,然后進程core掉,,,
因此這篇博客《一個C/C++協(xié)程庫的思考與實現(xiàn)之棧溢出檢測》
https://blog.csdn.net/DoasIsay/article/details/107396105
就需要更新一下了,因為我們找到了一種更好的方法mprotect來主動檢測棧溢出
對于操作系統(tǒng)的任務(wù)(進程或線程)而言,任務(wù)所需的棧內(nèi)存,堆內(nèi)存,并不是任務(wù)啟動后或發(fā)起內(nèi)存申請(brk/mmap/malloc/new)后操作系統(tǒng)立即為其分配物理內(nèi)存,而是先為其在進程的虛擬地址空間中找到一塊空閑的空間標記其大小起止地址及訪問權(quán)限,當CPU真正訪問到任務(wù)未分配物理內(nèi)存的虛擬頁內(nèi)的地址時MMU會產(chǎn)生一個內(nèi)存缺頁中斷,此時在缺頁中斷處理中操作系統(tǒng)才會真正的為任務(wù)分配一頁物理內(nèi)存并更新進程的頁表
對于在用戶空間實現(xiàn)的協(xié)程而言并不能使用操作系統(tǒng)及CPU提供的這種按需延遲分配的機制,但操作系統(tǒng)向用戶提供了信號處理這種軟中斷及mprotect這種接口,但是它還是不能支撐我們在用戶空間模擬實現(xiàn)這種機制,如在內(nèi)存越界訪問時,發(fā)出信號,由于協(xié)程棧是在堆上分配的,當棧溢出時就會發(fā)生堆內(nèi)存越界訪問,此時如果對協(xié)程的棧頂即堆的起始一段內(nèi)存進行mprotect,當棧溢出時就會觸發(fā)SIGSEGV信號,在信號處理函數(shù)中我們可以為當前觸發(fā)SIGSEGV信號的協(xié)程擴充棧空間
想法甚好,但是,,,
問題1
如何獲得觸發(fā)SIGSEGV信號的協(xié)程?
在多線程環(huán)境中當向一個進程發(fā)送信號后,信號被投遞到那個線程完成完全是隨機的,除了硬件錯誤與定時器觸發(fā)的信號,SIGSEGV是一個硬件錯誤?如果它不是一個硬件錯誤,有可能當前進行信號處理的線程就不是觸發(fā)SIGSEGV信號的協(xié)程所在的線程,我們獲取當前線程正在調(diào)度運行的協(xié)程是通過__thread線程局部變量current,此變量是一個指向當前線程正在執(zhí)行的程協(xié)對象的指針,因此處理SIGSEGV信號的線程一定要是觸發(fā)SIGSEGV信號的協(xié)程所在的線程,才能獲取到觸發(fā)SIGSEGV信號的協(xié)程
問題2
如何區(qū)分是因協(xié)程棧溢出導(dǎo)致觸發(fā)SIGSEGV信號,還是野指針導(dǎo)致的?
如果我們能在信號處理函數(shù)中得到導(dǎo)致觸發(fā)SIGSEGV信號的內(nèi)存地址,再與當前觸發(fā)SIGSEGV信號的協(xié)程的棧地址進行比較,如果相差不遠,那就不會是野指針導(dǎo)致的,但是我們無法在信號處理函數(shù)中獲取到觸發(fā)SIGSEGV信號的內(nèi)存地址
經(jīng)過測試,觸發(fā)SIGSEGV信號的線程會收到SIGSEGV信號,但是在信號處理函數(shù)中無法完成問題2的操作,而且就算問題2可以解決,我們在SIGSEGV的信號處理函數(shù)中為協(xié)程擴充了棧空間后,此線程也只會被內(nèi)核不斷的發(fā)送SIGSEGV信號,因為信號處理函數(shù)會返回到觸發(fā)SIGSEGV信號的那條指令繼續(xù)執(zhí)行,而我們無法修改這條指令所使用的地址,也就是在信號處理函數(shù)中對a變量的修改無法被fun函數(shù)再次獲取到
比如下代碼
?
因此就真的不能在用戶空間為協(xié)程實現(xiàn)棧的動態(tài)按需增長
一種比較樸素的實現(xiàn)方法,在協(xié)程的函數(shù)調(diào)用的入口加入檢測代碼就像棧溢出檢測那樣丑陋的代碼,檢測cpu當前sp寄存器的值與棧的未尾做對比,比如還剩80%的空間就進行棧的擴充,但是如下代碼會令你的檢測失效,比如棧空間是2kb,假設(shè)進入fun函數(shù)前已經(jīng)使用了1kb,進入fun函數(shù)后進行檢測發(fā)現(xiàn)只使用了50%的棧空間,但檢測后立即在棧上申請了1kb的空間,此時代碼繼續(xù)運行就有可能產(chǎn)生棧溢出
void fun(){
?????? check_stack();
?????? char a[1024];
?????? xxxxxxx;
?????? xxxxxxx;
}
因此應(yīng)盡量提高棧擴充的檢測條件,比如棧空間使用超過50%后就擴充,另外盡量不在棧中創(chuàng)建大的臨時變量
如何進行棧的擴充?
使用malloc分配新的棧空間拷貝老棧的內(nèi)容到新棧,這會有個問題就是棧中的局部變量的地址還是老棧的,因此我們需要修改每一個局部變量的地址?不,不需要
如下代碼
因為在函數(shù)調(diào)用的棧幀中是通過bp基棧指針+相對地址去訪問棧上的變量的,我們僅需修改協(xié)程的函數(shù)調(diào)用鏈中每個棧幀上保存的bp,new_bp=new_stack_start+(old_bp-old_stack_start)通過老的值計算出相對偏移量然后與新棧起始地址相加計算出新值回填到棧幀上,此處就涉及到棧的回溯,其實很簡單,取出當前棧幀上保存的上一個棧幀的bp,以此類推直至協(xié)程的入口函數(shù)調(diào)用的棧幀,然后再修改協(xié)程context的bp/sp就可以了
步驟如下:
在每個函數(shù)調(diào)用中加入檢測代碼是丑陋且繁瑣的,而且我們也無法在所有的函數(shù)調(diào)用中加入檢測代碼,因為還有第三方庫的函數(shù),因此在協(xié)程庫中用這種方式實現(xiàn)棧的動態(tài)擴充并不是很優(yōu)雅,除非是編譯器支持,如gcc提供的分段棧,就算這樣我們也不能保證我們使用的所有依賴庫在編譯時都打開了分段棧的選項,對于協(xié)程棧的動態(tài)擴充還是別想了吧
既然操作系統(tǒng)提供了虛擬內(nèi)存,任務(wù)申請的內(nèi)存只是虛擬內(nèi)存,申請了不用就不會占用物理內(nèi)存,那么我們直接給足協(xié)程的棧,不就行了?只不過會導(dǎo)致進程占用的虛擬內(nèi)存變大而已,還搞什么協(xié)程棧的動態(tài)按需增長,,,
總結(jié)
以上是生活随笔為你收集整理的一个C/C++协程库的思考与实现之协程栈的动态按需增长的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php实现页面强制跳转,PHP实现页面跳
- 下一篇: s3c2440移植MQTT