<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          跪了,米哈游是真的細節(jié)...

          共 9869字,需瀏覽 20分鐘

           ·

          2024-04-10 16:03

          fc97d287b39bbf208275a4abbe4148da.webp

          圖解學習網(wǎng)站: xiaolincoding.com

          大家好,我是小林。

          今天分享米哈游的校招面經(jīng),雖然問的問題不算很多,但是主要是考慮的計算機基礎(chǔ)的細節(jié),游戲類的公司都會比較注重性能,所以對于計算機基礎(chǔ)方面的內(nèi)容會考察比較多。

          米哈游面試

          輸入URL過程用到哪些協(xié)議?

          主要會涉及HTTP/HTTPS協(xié)議、DNS協(xié)議、TCP協(xié)議、ARP協(xié)議、OPSF協(xié)議。

          d39e756b95f57182e835b820cfc81943.webp

          輸入URL過程如下:

          5ba554b2bc9d9937a058ba380e92143d.webp
          • DNS 解析:當用戶輸入一個網(wǎng)址并按下回車鍵的時候,瀏覽器獲得一個域名,而在實際通信過程中,我們需要的是一個 IP 地址,因此我們需要先把域名轉(zhuǎn)換成相應(yīng) IP 地址。
          • TCP 連接:瀏覽器通過 DNS 獲取到 Web 服務(wù)器真正的 IP 地址后,便向 Web 服務(wù)器發(fā)起 TCP 連接請求,通過 TCP 三次握手建立好連接。
          • 建立TCP協(xié)議時,需要發(fā)送數(shù)據(jù),發(fā)送數(shù)據(jù)在網(wǎng)絡(luò)層使用IP協(xié)議, 通過IP協(xié)議將IP地址封裝為IP數(shù)據(jù)報;然后此時會用到ARP協(xié)議,主機發(fā)送信息時將包含目標IP地址的ARP請求廣播到網(wǎng)絡(luò)上的所有主機,并接收返回消息,以此確定目標的物理地址,找到目的MAC地址;
          • IP數(shù)據(jù)包在路由器之間,路由選擇使用OPSF協(xié)議, 采用Dijkstra算法來計算最短路徑樹,抵達服務(wù)端。
          • 發(fā)送 HTTP 請求:建立 TCP 連接之后,瀏覽器向 Web 服務(wù)器發(fā)起一個 HTTP 請求(如果是HTTPS協(xié)議,發(fā)送HTTP 請求之前還需要完成TLS四次握手);
          • 處理請求并返回:服務(wù)器獲取到客戶端的 HTTP 請求后,會根據(jù) HTTP 請求中的內(nèi)容來決定如何獲取相應(yīng)的文件,并將文件發(fā)送給瀏覽器。
          • 瀏覽器渲染:瀏覽器根據(jù)響應(yīng)開始顯示頁面,首先解析 HTML 文件構(gòu)建 DOM 樹,然后解析 CSS 文件構(gòu)建渲染樹,等到渲染樹構(gòu)建完成后,瀏覽器開始布局渲染樹并將其繪制到屏幕上。

          TCP中斷了怎么辦?發(fā)送方幾個報文都沒回復怎么辦?

          如果TCP意外斷開,并沒有正常關(guān)閉socket,雙方并未按照協(xié)議上的四次揮手去斷開連接。

          那么這時候正在執(zhí)行Recv或Send操作的一方就會因為沒有任何連接中斷的通知而一直等待下去,也就是會被長時間卡住。

          像這種如果一方已經(jīng)關(guān)閉或異常終止連接,而另一方卻不知道,我們將這樣的TCP連接稱為半打開的。

          解決意外中斷辦法都是利用保活機制。而保活機制分又可以讓底層實現(xiàn)也可自己實現(xiàn)。

          自己編寫心跳包程序

          在自己的程序中加入一條線程,定時向?qū)Χ税l(fā)送數(shù)據(jù)包,查看是否有ACK,如果有則連接正常,沒有的話則連接斷開。

          比如,web 服務(wù)軟件一般都會提供 keepalive_timeout 參數(shù),用來指定 HTTP 長連接的超時時間。如果設(shè)置了 HTTP 長連接的超時時間是 60 秒,web 服務(wù)軟件就會啟動一個定時器,如果客戶端在完成一個 HTTP 請求后,在 60 秒內(nèi)都沒有再發(fā)起新的請求,定時器的時間一到,就會觸發(fā)回調(diào)函數(shù)來釋放該連接。

          c228bdb6daf6a23880f1f78743f39674.webpweb 服務(wù)的 心跳機制

          啟動 TCP 的keepAlive機制

          以服務(wù)器端為例,如果當前server端檢測到超過一定時間沒有數(shù)據(jù)傳輸,那么會向client端發(fā)送一個心跳探測報文,如果連續(xù)幾個探測報文都沒有得到響應(yīng),則認為當前的 TCP 連接已經(jīng)死亡,系統(tǒng)內(nèi)核將錯誤信息通知給上層應(yīng)用程序。

          在 Linux 內(nèi)核可以有對應(yīng)的參數(shù)可以設(shè)置保活時間、保活探測的次數(shù)、保活探測的時間間隔,以下都為默認值:

                
                net.ipv4.tcp_keepalive_time=7200
          net.ipv4.tcp_keepalive_intvl=75  
          net.ipv4.tcp_keepalive_probes=9
          • tcp_keepalive_time=7200:表示保活時間是 7200 秒(2小時),也就 2 小時內(nèi)如果沒有任何連接相關(guān)的活動,則會啟動保活機制
          • tcp_keepalive_intvl=75:表示每次檢測間隔 75 秒;
          • tcp_keepalive_probes=9:表示檢測 9 次無響應(yīng),認為對方是不可達的,從而中斷本次的連接。

          也就是說在 Linux 系統(tǒng)中,最少需要經(jīng)過 2 小時 11 分 15 秒才可以發(fā)現(xiàn)一個「死亡」連接。

          c2bac5896ac1edf7bc642ee670db645e.webpimg

          注意,應(yīng)用程序若想使用 TCP 保活機制需要通過 socket 接口設(shè)置 SO_KEEPALIVE 選項才能夠生效,如果沒有設(shè)置,那么就無法使用 TCP 保活機制。

          如果開啟了 TCP 保活,需要考慮以下幾種情況:

          • 第一種,對端程序是正常工作的。當 TCP 保活的探測報文發(fā)送給對端, 對端會正常響應(yīng),這樣 TCP 保活時間會被重置,等待下一個 TCP 保活時間的到來。
          • 第二種,對端主機宕機并重啟。當 TCP 保活的探測報文發(fā)送給對端后,對端是可以響應(yīng)的,但由于沒有該連接的有效信息,會產(chǎn)生一個 RST 報文,這樣很快就會發(fā)現(xiàn) TCP 連接已經(jīng)被重置。
          • 第三種,是對端主機宕機(注意不是進程崩潰,進程崩潰后操作系統(tǒng)在回收進程資源的時候,會發(fā)送 FIN 報文,而主機宕機則是無法感知的,所以需要 TCP 保活機制來探測對方是不是發(fā)生了主機宕機),或?qū)Χ擞捎谄渌驅(qū)е聢笪牟豢蛇_。當 TCP 保活的探測報文發(fā)送給對端后,石沉大海,沒有響應(yīng),連續(xù)幾次,達到保活探測次數(shù)后,TCP 會報告該 TCP 連接已經(jīng)死亡

          鎖有哪些?

          Linux 提供的鎖機制,主要有互斥鎖、信號量、自旋鎖、讀寫鎖。

          • 互斥鎖(Mutex):也稱為互斥量,用于保護共享資源,確保同一時間只有一個線程可以訪問該資源。當一個線程獲取到互斥鎖后,其他線程需要等待鎖釋放才能繼續(xù)訪問。
          • 信號量(Semaphore):用于控制對一組資源的訪問。信號量可以允許多個線程同時訪問資源,但是需要在訪問前進行P操作(申請資源)和在訪問結(jié)束后進行V操作(釋放資源),以確保資源的正確使用。
          • 自旋鎖(Spinlock):是一種忙等待鎖,在獲取鎖之前,線程會一直嘗試獲取鎖,而不會進入睡眠狀態(tài)。自旋鎖適用于保護臨界區(qū)較小、鎖占用時間短暫的情況。
          • 讀寫鎖(ReadWrite Lock):也稱為共享-獨占鎖,用于在讀操作和寫操作之間提供更好的并發(fā)性。讀寫鎖允許多個線程同時讀取共享資源,但是在寫操作時需要獨占訪問,保證數(shù)據(jù)的一致性。

          鎖本身怎么保證線程安全?為什么一個線程拿到鎖以后,另一個線程就無法獲得鎖?

          所謂的鎖,在計算機里本質(zhì)上就是一塊內(nèi)存空間。當這個空間被賦值為1的時候表示加鎖了,被賦值為0的時候表示解鎖了,僅此而已。多個線程搶一個鎖,就是搶著要把這塊內(nèi)存賦值為1。在一個多核環(huán)境里,內(nèi)存空間是共享的。

          CPU 通常會有多個CPU核心,每個核上各跑一個線程,那如何保證一次只有一個線程成功搶到鎖呢?這必須要硬件的支持,譬如提供某種特殊指令。具體的實現(xiàn)如下。

          硬件實現(xiàn)

          CPU如果提供一些用來構(gòu)建鎖的原子性指令,比如x86的XCHG或CMPXCHG(加上LOCK prefix),能夠完成原子性的compare-and-swap (CAS)機制,用這樣的硬件指令就能實現(xiàn)自旋鎖。

          畫外音:那么CAS是什么呢?

          • CAS有3個操作數(shù),內(nèi)存值V,舊的預期值A(chǔ),要修改的新值B。當且僅當預期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則什么都不做。反正就是我知道你flag為0,我來了,誒?你的flag怎么是1?那我走了,再見。

          LOCK前綴的作用是鎖定系統(tǒng)總線(或者鎖定某一塊cache line)來實現(xiàn)原子性,簡單來說就是,如果指令前加了LOCK前綴,就是告訴其他核,一旦我開始執(zhí)行這個指令了,在我結(jié)束這個指令之前,誰也不許動,這樣便實現(xiàn)了一次只能有一個核對同一個內(nèi)存地址賦值。

          操作系統(tǒng)實現(xiàn)

          CPU提供了特殊硬件來保證內(nèi)存操作的原子性的時候,再通過一種簡單的邏輯,我們就可以構(gòu)建出一個簡單的自旋鎖來了。

          下面就可以利用CAS來實現(xiàn)自旋鎖了:

                
                // 1表示自旋鎖已分配給某個線程,0表示自旋鎖沒有分配個任何線程,即處于釋放狀態(tài)
          int owner = 0;

          void spin_lock(int * owner ) {
          while(!compare_and_set(owner, 0, 1) {
          }
          }//需要說明的一點是系統(tǒng)對comare_and_set的內(nèi)核實現(xiàn)中包含對總線加鎖

          這個邏輯很簡單,我們讓多個線程持續(xù)地搶著將某塊內(nèi)存地址的值賦為1(原始為0),誰先搶到了誰就可以進入臨界區(qū),沒搶到的線程繼續(xù)在一個循環(huán)里面反復讀取這塊內(nèi)存地址的值,看看自己的機會是不是到了。當搶到鎖的線程在它的臨界區(qū)結(jié)束以后,就可以把該內(nèi)存地址的值賦為0,表示現(xiàn)在其他人可以搶鎖了,用這種方式構(gòu)建出來的鎖也被稱為自旋鎖,自旋的意思就是沒有拿到鎖的線程就在那兒自己轉(zhuǎn)兒。

          接下來我們可以利用 CAS 原子操作實現(xiàn)互斥鎖,我們可以通過操作系統(tǒng)的介入,來實現(xiàn)一個線程向操作系統(tǒng)申請把自己掛起,還是把自己掛起,將CPU讓出來,這樣或許其他的線程能做點更有用的事情。通過這樣一種機制構(gòu)建出來的鎖,常常被稱為mutex(互斥鎖)。

                
                lock:
                movb $0, $al
                xchgb $al, mutex
                if(al 寄存器的內(nèi)存 > 0) 
          {
                  return0;
                } else
                {
                  掛起等待;
                }
                goto lock;
          unlock
             movb $1, mutex
             喚醒等待 mutex 的線程;
             return 0;
           

          lock:這一步先將0賦值到寄存器al,再將mutex與al中的值交換(xchgb指令,CAS原子操作),再進行判斷。當對某個線程進行l(wèi)ock操作時,即使在中間任意一步被切出去也沒有問題。這樣就保證了lock的操作也是原子的。

          多線程一定好嗎?怎么選用?

          不一定的,線程越多,系統(tǒng)的運行速度不一定越快。

          3354747fcd30e5563f7e74a6c43ae3be.webp

          多線程的優(yōu)點是提高程序的并發(fā)性,因為多線程可以同時執(zhí)行多個任務(wù),分利用多核處理器的性能,提高程序的處理能力和響應(yīng)速度,所以如果任務(wù)可以進行有效的并行處理,并且對響應(yīng)時間有較高的要求,多線程可以提升性能和用戶體驗。

          而多線程的缺點主要是:

          • 多線程編程復雜:多線程編程需要考慮線程同步、共享資源訪問等問題,容易引發(fā)死鎖、競態(tài)條件等并發(fā)問題,增加了程序的復雜性和調(diào)試難度。
          • 資源消耗:多線程需要占用額外的內(nèi)存和CPU資源,線程的創(chuàng)建、銷毀和切換都需要一定的開銷,如果線程數(shù)量過多,會導致系統(tǒng)資源的浪費和性能下降。

          線程切換詳細過程是怎么樣的?上下文保存在哪里?

          ee6076f4d54922760d1d3ef8c619c8b1.webp

          線程切換的詳細過程可以分為以下幾個步驟:

          • 上下文保存:當操作系統(tǒng)決定切換到另一個線程時,它首先會保存當前線程的上下文信息。上下文信息包括寄存器狀態(tài)、程序計數(shù)器、堆棧指針等,用于保存線程的執(zhí)行狀態(tài)。
          • 切換到調(diào)度器:操作系統(tǒng)將執(zhí)行權(quán)切換到調(diào)度器(Scheduler)。調(diào)度器負責選擇下一個要執(zhí)行的線程,并根據(jù)調(diào)度算法做出決策。
          • 上下文恢復:調(diào)度器選擇了下一個要執(zhí)行的線程后,它會從該線程保存的上下文信息中恢復線程的執(zhí)行狀態(tài)。
          • 切換到新線程:調(diào)度器將執(zhí)行權(quán)切換到新線程,使其開始執(zhí)行。

          上下文信息的保存通常由操作系統(tǒng)負責管理,具體保存在哪里取決于操作系統(tǒng)的實現(xiàn)方式。一般情況下,上下文信息會保存在線程的控制塊(Thread Control Block,TCB)中。

          TCB是操作系統(tǒng)用于管理線程的數(shù)據(jù)結(jié)構(gòu),包含了線程的狀態(tài)、寄存器的值、堆棧信息等。當發(fā)生線程切換時,操作系統(tǒng)會通過切換TCB來保存和恢復線程的上下文信息。

          如果要對一個很大的數(shù)據(jù)集,進行排序,而沒辦法一次性在內(nèi)存排序,這時候怎么辦?

          可以使用外部排序來解決,基本思路分為兩個階段。

          部分排序階段。

          我們根據(jù)內(nèi)存大小,將待排序的文件拆成多個部分,使得每個部分都是足以存入內(nèi)存中的。然后選擇合適的內(nèi)排序算法,將多個文件部分排序,并輸出到容量可以更大的外存臨時文件中,每個臨時文件都是有序排列的,我們將其稱之為一個“順段”。

          在第一個階段部分排序中,由于內(nèi)存可以裝下每個順段的所有元素,可以使用快速排序,時間復雜度是O(nlogn)。

          歸并階段

          我們對前面的多個“順段”進行合并,思想和歸并排序其實是一樣的。以 2 路歸并為例,每次都將兩個連續(xù)的順段合并成一個更大的順段。

          因為內(nèi)存限制,每次可能只能讀入兩個順段的部分內(nèi)容,所以我們需要一部分一部分讀入,在內(nèi)存里將可以確定順序的部分排列,并輸出到外存里的文件中,不斷重復這個過程,直至兩個順段被完整遍歷。這樣經(jīng)過多層的歸并之后,最終會得到一個完整的順序文件。

          ad882b6d289a2098152fa19e72c89106.webp

          歸并階段有個非常大的時間消耗就是 IO,也就是輸入輸出。最好就是讓歸并的層數(shù)越低越好,為了降低降低歸并層數(shù),可以使用敗者樹

          敗者樹中的非終端結(jié)點中存儲的是勝利(左右孩子相比較,誰最小即為勝者)的一方;而敗者樹中的非終端結(jié)點存儲的是失敗的一方。而在比較過程中,都是拿勝者去比較。

          18d60e20479a07d5ed9dfbfa06917e0c.webp

          現(xiàn)在有了敗者樹的加持,多路歸并排序就可以比較高效地解決外部排序的問題了。

          大致思路就是:

          • 先用內(nèi)排序算法(比如快速排序),盡可能多的加載源文件,將其變成 n 個有序順段。
          • 在內(nèi)存有限的前提下每 k 個文件為一組,每次流式地從各個文件中讀取一個單詞,借助敗者樹選出字典序最低的一個,輸出到文件中,這樣就可以將 k 個順段合并到一個順段中了;反復執(zhí)行這樣的操作,直至所有順段被歸并到同一個順段。

          算法

          手撕LRU算法(思路:鏈表+哈希表)

          c094c0487bc85be45f09560ad05ebeeb.webp
                
                #include <iostream>
          #include <unordered_map>
          #include <list>

          using namespace std;

          class LRUCache {
          private:
              int capacity;
              unordered_map<int, pair<intlist<int>::iterator>> cache;
              list<int> lru;

          public:
              LRUCache(int cap) {
                  capacity = cap;
              }

              int get(int key) {
                  if (cache.find(key) != cache.end()) {
                      // 將訪問的元素移到鏈表頭部
                      lru.erase(cache[key].second);
                      lru.push_front(key);
                      cache[key].second = lru.begin();
                      return cache[key].first;
                  }
                  return -1;
              }

              void put(int key, int value) {
                  if (cache.find(key) != cache.end()) {
                      // 更新已存在的元素的值,并將其移到鏈表頭部
                      lru.erase(cache[key].second);
                  }
                  else if (cache.size() >= capacity) {
                      // 刪除最久未使用的元素
                      cache.erase(lru.back());
                      lru.pop_back();
                  }
                  // 插入新元素到鏈表頭部
                  lru.push_front(key);
                  cache[key] = make_pair(value, lru.begin());
              }
          };

          int main() {
              LRUCache cache(2)// 創(chuàng)建容量為2的LRU緩存

              cache.put(11);
              cache.put(22);
              cout << cache.get(1) << endl// 輸出 1
              cache.put(33); // 因為緩存容量已滿,所以刪除最久未使用的元素2
              cout << cache.get(2) << endl// 輸出 -1,因為元素2已被刪除
              cache.put(44); // 因為緩存容量已滿,所以刪除最久未使用的元素1
              cout << cache.get(1) << endl// 輸出 -1,因為元素1已被刪除
              cout << cache.get(3) << endl// 輸出 3
              cout << cache.get(4) << endl// 輸出 4

              return 0;
          }

          歷史好文:

          運氣真好!B站給機會了?

          又被百度撈起來了,能贏嗎?

          等不及,沖滴滴去了!

          不愧是微信啊,問的范圍賊廣!

          瀏覽 34
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  可 尻屄网站 | 91蝌蚪91 | 久久久无码电影 | 91|久久|麻豆 | 黄片视频大全 |