【面試】網易遊戲面試題目整理及答案(5)

網易遊戲面試題目整理及答案(5)

算法

  1. Leetcode 75題:請寫出一個高效的在m*n矩陣中判斷目標值是否存在的算法,矩陣具有如下特徵:
    1)每一行的數字都是從左到右排序的
    2)每一行的第一個數字都比上一行最後一個數字大
    例如:
    對於下面的矩陣:
    [ [1,3,5,7], [10,11,16,20], [23, 30, 34, 50]]
    要搜索的目標值爲3,返回true.
    答:首先取右上角的數字,如果該數字等於要查找的數字,查找過程結束;如果該數字大於要查找的數字,去掉此數字所在的列;如果該數字小於要查找的數字,則去掉該數字所在的行。重複上述過程直到找到要查找的數字,或者查找範圍爲空。
    代碼如下:
# LeedCode 75
class Solution:
    def searchMatrix(self, matrix, target):
        m = len(matrix)-1 # 行數
        n = len(matrix[0])-1 # 列數
        i=0
        j=n
        while(i<=m and j>=0):
            if matrix[i][j]>target:
                j-=1
            elif matrix[i][j]<target:
                i+=1
            else:
                return True
        return False
  1. Leetcode 55 :給出一顆二叉樹,返回這棵樹的中序遍歷
    例如:給出的二叉樹爲{1,#,2,3}
    二叉樹
    返回[1,3,2]
    備註:使用遞歸和迭代的方法
    如果你不清楚“{1,#,2,3}”的含義,繼續閱讀:
    我們使用如下方法將二叉樹序列化:二叉樹的序列化遵循層序遍歷的原則,“#”代表該位置是一條路徑的終結,下面不再存在結點。
    例如:
    1↵ / ↵ 2 3↵ /↵ 4↵ ↵ 5
    上述的二叉樹序列化的結果是:"{1,2,3,#,#,4,#,#,5}".
    答:使用一個輔助棧來模擬遞歸過程
    代碼如下:
# LeedCode 55
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

class Solution:
    def inorderTraversal(self, root):
        if root is None:
            return []
        # return (self.inorderTraversal(root.left)+[root.val]+self.inorderTraversal(root.right)) # 遞歸方式
        # 非遞歸方式
        queue = []
        res = []
        while len(queue) > 0 or root:
            while root:
                queue.append(root)
                root = root.left
            root = queue.pop()
            res.append(root.val)
            root = root.right
        return res
  1. 快速排序的原理,手寫快速排序,哪些排序是穩定的?
    答:快速排序是通過一趟排序將要排序的數據分割成獨立的兩部分,分割點左邊都是比它小的數,右邊都是比它大的數。然後按照此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。具體步驟如下:
    ①挑選基準值:從數列中挑出一個元素,成爲“基準”(pivot)
    ②分割:重新排序數列,所有比基準值小的元素排放在基準前面,所有比基準值大的元素擺在基準後面(與基準值相等的數可以放在任意一邊)。在這個分割結束之後,對基準值的排序就已經完成了;
    ③遞歸排序子序列:遞歸地將小於基準值元素的子序列和大於基準值元素的子序列排序。
    遞歸到最底部的判斷條件是數列的大小是0或1,此時該數列顯然已經有序。
    代碼如下:
# 快速排序
# arr[] :待排序的數字
# low : 起始索引
# high: 結束索引
def quickSort( arr, low, high):
    if low < high:
        pi = partition(arr, low, high)
        quickSort(arr, low, pi-1)
        quickSort(arr, pi+1, high)
    
def partition( arr, low, high):
    i = (low-1) # 最小元素索引
    pivot = arr[high]
      
    for j in range(low, high):
        # 當前元素小於或等於pivot
        if arr[j] <= pivot:
            i = i+1
            arr[i],arr[j] = arr[j],arr[i]
    arr[i+1],arr[high] = arr[high],arr[i+1]
    return (i+1)

arr = [10,7,8,9,1,5]
n = len(arr)
quickSort(arr, 0, n-1)
print("排序後的數組:")
for i in range(n):
    print("%d" %arr[i])

初次之外,還有交換排序(冒泡排序、快速排序),插入排序(直接插入排序,希爾排序),選擇排序(簡單選擇排序,堆排序),歸併排序,基數排序。其中屬於穩定性的是:冒泡排序、直接插入排序、歸併排序、基數排序。
具體的比較如下表所示:
排序算法比較
17. LeetCode: MajorityElement .已知一個數組的大小,並且其中存在一個數,出現的頻率大於50%,則稱其爲該數組的主元素。用一個算法找出這個數,要求其時間複雜度儘可能低。
答:有四種方法:
1)將數組排序,取位於數組中間的元素即可。時間複雜度爲O(NlogN)。
2)統計每個元素出現的次數,然後選出出現次數最多的那個即可,時間複雜度爲O(N)。
3)因爲這個元素出現的次數超過整個數組長度的一半,所以若數組中所有的這個元素若集中在一起,則數組中間的元素必定是這個元素。因此可以考慮快速排序的Partition算法:若某次Partition選出的Pivot在Partition後位於數組中間,則即是這個出現次數超過一半的元素。時間複雜度爲O(n)。
4)數組中一個元素出現次數超過數組長度的一半,也就是說它出現的次數比其他所有數字出現的次數的和還要多。因此可以在遍歷時保存兩個值:一個是數組中的一個數字,一個是次數。當遍歷到下一個數字的時候,如果它和之前保存的數字相同,則次數加1;否則次數減1。如果次數爲0,則保存下一個數字,並將次數設爲1。最後要找的數字肯定是最後一次把次數設爲1時對應的元素。時間複雜度爲O(N)。
代碼如下:

def majorityElement( nums):
    """
    :type nums : List[int]
    :rtype: int
    """
    elem = 0
    count = 0
    for i in nums:
        if count == 0:
            elem = i
            count = 1
        else:
            if elem == i:
                count += 1
            else:
                count -= 1
    return elem
  1. LRU(O(1)時間複雜度)
    答:這是一個LeetCode非常經典的題目。即==設計和實現一個 LRU (最近最少使用) 緩存機制。它應該支持以下操作: 獲取數據 get 和 寫入數據 put ==。
    獲取數據 get(key) - 如果關鍵字 (key) 存在於緩存中,則獲取關鍵字的值(總是正數),否則返回 -1。
    寫入數據 put(key, value) - 如果關鍵字已經存在,則變更其數據值;如果關鍵字不存在,則插入該組「關鍵字/值」。當緩存容量達到上限時,它應該在寫入新數據之前刪除最久未使用的數據值,從而爲新的數據值留出空間。要求時間複雜度爲O(1)。
    示例:
    LRUCache cache = new LRUCache( 2 /* 緩存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 該操作會使得關鍵字 2 作廢
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 該操作會使得關鍵字 1 作廢
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

代碼如下:

package DataStructure;

import java.util.*;

/**
 * LRU Cache
 * 方法:雙向鏈表+HashMap
 */
class CacheNode {
    /**
     * 新建CacheNode節點,Key-Value值,並有指向前驅節點和後繼節點的指針,構成雙向鏈表的節點
     */
    int key;
    int value;
    CacheNode pre;
    CacheNode next;

    public CacheNode() {
    }

    public CacheNode(int key, int value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public String toString() {
        return this.key + "-" + this.value + " ";
    }
}

public class LRUCache {
    int capacity;
    int count = 0;
    HashMap<Integer, CacheNode> map = new HashMap<>();
    CacheNode head = null;
    CacheNode end = null;

    // 方法2的構造方法
    public LRUCache(int capacity) {
        this.capacity = capacity;
    }
    // 方法1的構造方法
//    public LRUCache(int capacity) {
//        this.capacity = capacity;
//        this.count = 0;
//        head = new CacheNode();
//        end = new CacheNode();
//        head.next = end;
//        end.pre = head;
//    }

    /**
     * 每次數據項被查詢時,都將此數據項移動到鏈表頭部(O(1)的時間複雜度)
     * 這樣,在進行多次查找操作後,最近被使用過的內容就像連表的頭移動,而沒有被使用過的內容就像鏈表的後面移動
     * <p>
     * 如果cache中不存在要get的值,返回-1;如果存在,則返回其值並將其在原鏈表中刪除,然後將其插入作爲頭節點
     *
     * @param key
     * @return
     */
    public int get_method1(int key) {
        if (map.containsKey(key)) {
            CacheNode n = map.get(key);
            moveToHead(n);
            printNodes("get1");
            return n.value;
        }
        printNodes("get1");
        return -1;
    }

    public int get_method2(int key) {
        if (map.containsKey(key)) {
            CacheNode n = map.get(key);
            remove(n);
            setHead(n);
            printNodes("get2");
            return n.value;
        }
        printNodes("get2");
        return -1;
    }

    /**
     * 當需要替換時,鏈表最後的位置就是最近最少被使用的數據項,只需要將最新的數據項放在來鏈表頭部,當Cache滿時,淘汰鏈表最後的位置
     *
     * @param n
     */
    public void remove(CacheNode n) {
        if (n.pre != null) {
            n.pre.next = n.next;
        } else {
            head = n.next;
        }
        if (n.next != null) {
            n.next.pre = n.pre;
        } else {
            end = n.pre;
        }
    }

    public void setHead(CacheNode n) {
        n.next = head;
        n.pre = null;
        if (head != null) {
            head.pre = n;
        }
        head = n;
        if (end == null) {
            end = head;
        }
    }

    private void removeNode(CacheNode node) {
        CacheNode preNode = node.pre;
        preNode.next = node.next;
        node.next.pre = preNode;
    }

    private void moveToHead(CacheNode node) {
        removeNode(node);
        addNode(node);
    }

    private void addNode(CacheNode node) {
        CacheNode old = head.next;
        head.next = node;
        node.pre = head;
        node.next = old;
        old.pre = node;
    }

    private CacheNode popTail() {
        CacheNode node = end.pre;
        removeNode(node);
        return node;
    }

    /**
     * 當set的key已經存在,就更新其value,並將其在原鏈表中刪除,然後將其作爲頭節點;
     * 當set的key不存在,就新建一個CacheNode,如果當前的cache.size<capacity,就將其加入到hashMap中,並將其作爲頭節點,更新cache的size;
     * 否則,刪除鏈表最後一個node,再將要set的key放入hashMap中,並作爲鏈表的頭節點,但是cache的size不更新。
     * <p>
     * 原則就是:每當訪問鏈表時都更新鏈表節點
     *
     * @param key
     * @param value
     */
    public void set_method1(int key, int value) {
        CacheNode created = new CacheNode(key, value);
        if (map.containsKey(key)) {
            CacheNode node = map.get(key);
            node.value = value;
        } else {
            if (count >= capacity) {
                map.remove(popTail().key);
                --count;
            }
            map.put(key, created);
            addNode(created);
            count++;
        }
        printNodes("set1");
    }

    public void set_method2(int key, int value) {
        if (map.containsKey(key)) {
            CacheNode oldNode = map.get(key);
            oldNode.value = value;
            remove(oldNode);
            setHead(oldNode);
        } else {
            CacheNode created = new CacheNode(key, value);
            if (map.size() >= capacity) {
                map.remove(end.key);
                remove(end);
            }
            setHead(created);
            map.put(key, created);
        }
        printNodes("set2");
    }

    public void printNodes(String explain) {
        System.out.print(explain + ":" + head.toString());
        CacheNode node = head.next;
        while (node != null) {
            System.out.print(node.toString());
            node = node.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = Integer.parseInt(sc.nextLine().trim());
        LRUCache lruCache1 = new LRUCache(n);
        List<Integer> list1 = new ArrayList<>();
        LRUCache lruCache2 = new LRUCache(n);
        List<Integer> list2 = new ArrayList<>();
        while (sc.hasNextLine()) {
            String[] act = sc.nextLine().split(" ");
            String oper = act[0];
            if ("p".equals(oper)) {
                int key = Integer.parseInt(act[1]);
                int value = Integer.parseInt(act[2]);
                //lruCache1.set_method1(key, value);
                lruCache2.set_method2(key, value);
            } else if ("g".equals(oper)) {
                int key = Integer.parseInt(act[1]);
                //list1.add(lruCache1.get_method1(key));
                list2.add(lruCache2.get_method2(key));
            } else {
                break;
            }
        }
        sc.close();
        for (Integer i : list1) {
            System.out.println("方法1:" + i);
        }
        for (Integer j : list2) {
            System.out.println("方法2:" + j);
        }
    }
}

輸入:
2
p 1 1
p 2 2
g 1
p 3 3
g 2
p 4 4
g 1
g 3
g 4
輸出:
1
-1
-1
3
4

操作系統

  1. 從用戶態到內核態的彙編級過程
    答:在進程從用戶態到內核態切換過程中,Linux主要做的事:
    ①讀取tr寄存器,訪問TSS段
    ②從TSS段中的sp0獲取進程內核棧的棧頂指針
    ③由控制單元在內核棧中保存當前eflags,cs,ss,eip,esp寄存器的值
    ④由SAVE_ALL保存其寄存器的值到內核棧
    ⑤把內核代碼選擇符寫入CS寄存器,內核棧指針寫入ESP寄存器,把內核入口點的線性地址寫入EIP寄存器
    此時,CPU已經切換到內核態,根據EIP中的值開始執行內核入口點的第一條指令。
    esp寄存器是CPU棧指針,存放內核棧棧頂地址。在X86體系中,棧開始於末端,並朝內存區開始的方向增長。從用戶態剛切換到內核態時,進程的內核棧總是空的,此時esp指向這個棧的頂端。   
    在X86中調用int指令型系統調用後會把用戶棧的%esp的值及相關寄存器壓入內核棧中,系統調用通過iret指令返回,在返回之前會從內核棧彈出用戶棧的%esp和寄存器的狀態,然後進行恢復。所以在進入內核態之前要保存進程的上下文,中斷結束後恢復進程上下文,那靠的就是內核棧。   
    這裏有個細節問題,就是要想在內核棧保存用戶態的esp,eip等寄存器的值,首先得知道內核棧的棧指針,那在進入內核態之前,通過什麼才能獲得內核棧的棧指針呢?答案是:TSS
    X86體系結構中包括了一個特殊的段類型:任務狀態段(TSS),用它來存放硬件上下文。TSS反映了CPU上的當前進程的特權級linux爲每一個cpu提供一個tss段,並且在tr寄存器中保存該段。 在從用戶態切換到內核態時,可以通過獲取TSS段中的esp0來獲取當前進程的內核棧 棧頂指針,從而可以保存用戶態的cs,esp,eip等上下文。
    注:linux中之所以爲每一個cpu提供一個tss段,而不是爲每個進程提供一個tss段,主要原因是tr寄存器永遠指向它,在任務切換的適合不必切換tr寄存器,從而減小開銷。

  2. 中斷、異常以及系統調用
    答:所謂中斷是指CPU對系統發生的某個事件做出的一種反應,CPU暫停正在執行的程序,保留現場後自動地轉去執行相應的處理程序,處理完該事件後再返回斷點繼續執行被“打斷”的程序。中斷可分爲三類:
    第一類是由CPU外部引起的,稱作中斷,如I/O中斷、時鐘中斷、控制檯中斷等。
    第二類是來自CPU的內部事件或程序執行中的事件引起的過程,稱作
    異常
    ,如由於CPU本身故障(電源電壓低於105V或頻率在47~63Hz之外)、程序故障(非法操作碼、地址越界、浮點溢出等)等引起的過程。
    第三類由於在程序中使用了請求系統服務的系統調用而引發的過程,稱作**“陷入”(trap,或者陷阱)。前兩類通常都稱作中斷,它們的產生往往是無意、被動的,而陷入是有意和主動的**。
    1.中斷處理
    中斷處理一般分爲中斷響應和中斷處理兩個步驟。中斷響應由硬件實施,中斷處理主要由軟件實施。
    (1)中斷響應
    對中斷請求的整個處理過程是由硬件和軟件結合起來而形成的一套中斷機構實施的。發生中斷時,CPU暫停執行當前的程序,而轉去處理中斷。這個由硬件對中斷請求作出反應的過程,稱爲中斷響應。一般說來,中斷響應順序執行下述三步動作:
    ◆中止當前程序的執行;
    ◆保存原程序的斷點信息(主要是程序計數器PC和程序狀態寄存器PS的內容);
    ◆從中斷控制器取出中斷向量,轉到相應的處理程序。
    通常CPU在執行完一條指令後,立即檢查有無中斷請求,如果有,則立即做出響應。
    當發生中斷時,系統作出響應,不管它們是來自硬件(如來自時鐘或者外部設備)、程序性中斷(執行指令導致“軟件中斷”—Software Interrupts),或者來自意外事件(如訪問頁面不在內存)。
    如果當前CPU的執行優先級低於中斷的優先級,那麼它就中止對當前程序下條指令的執行,接受該中斷,並提升處理機的執行級別(一般與中斷優先級相同),以便在CPU處理當前中斷時,能屏蔽其它同級的或低級的中斷,然後保存斷點現場信息,通過取得的中斷向量轉到相應的中斷處理程序的入口。
    (2)中斷處理
    CPU從中斷控制器取得中斷向量,然後根據具體的中斷向量從中斷向量表IDT中找到相應的表項,該表項應是一箇中斷門。於是,CPU就根據中斷門的設置而到達了該通道的總服務程序的入口
    核心對中斷處理的順序主要由以下動作完成:
    ◆保存正在運行進程的各寄存器的內容,把它們放入核心棧的新幀面中。
    ◆確定“中斷源”或覈查中斷髮生,識別中斷的類型(如時鐘中斷或盤中斷)和中斷的設備號(如哪個磁盤引起的中斷)。系統接到中斷後,就從機器那裏得到一箇中斷號,它是檢索中斷向量表的位移。中斷向量因機器而異,但通常都包括相應中斷處理程序入口地址和中斷處理時處理機的狀態字。
    ◆核心調用中斷處理程序,對中斷進行處理。
    ◆中斷處理完成並返回。中斷處理程序執行完以後,核心便執行與機器相關的特定指令序列,恢復中斷時寄存器內容和執行核心棧退棧,進程回到用戶態。如果設置了重調度標誌,則在本進程返回到用戶態時做進程調度。
    2.系統調用
    在Unix/Linux系統中,系統調用像普通C函數調用那樣出現在C程序中。但是一般的函數調用序列並不能把進程的狀態從用戶態變爲內核態,而系統調用卻可以做到。
    C語言編譯程序利用一個預先確定的函數庫(一般稱爲C庫),其中有各系統調用的名字。C庫中的函數都專門使用一條指令,把進程的運行狀態改爲內核態。Linux的系統調用是通過中斷指令“INT 0x80”實現的
    每個系統調用都有惟一的號碼,稱作系統調用號。所有的系統調用都集中在系統調用入口表中統一管理。
    系統調用入口表是一個函數指針數組,以系統調用號爲下標在該數組中找到相應的函數指針,進而就能確定用戶使用的是哪一個系統調用。不同系統中系統調用的個數是不同的,目前Linux系統中共定義了221個系統調用。
    另外,系統調用表中還留有一些餘項,可供用戶自行添加。
    當CPU執行到中斷指令“INT 0x80”時,硬件就做出一系列響應,其動作與上述的中斷響應相同。CPU穿過陷阱門,從用戶空間進入系統空間。相應地,進程的上下文從用戶堆棧切換到系統堆棧。
    接着運行內核函數system_call()。首先,進一步保存各寄存器的內容;接着調用syscall_trace( ),以系統調用號爲下標檢索系統調用入口表sys_call_table,從中找到相應的函數;然後轉去執行該函數,完成具體的服務。
    執行完服務程序,核心檢查是否發生錯誤,並作相應處理。如果本進程收到信號,則對信號作相應處理。最後進程從系統空間返回到用戶空間。
    3.總結
    ①中斷是由間隔定時器和和I/O設備產生的。
    ②異常則是由程序的錯誤產生,或者由內核必須處理的異常條件產生。第一種情況下,內核通過發送一個信號來處理異常;第二種情況下,③內核執行恢復異常需要的所有步驟,或對內核服務的一個請求。
    ④中斷和異常改變處理器執行的指令順序,通常與CPU芯片內部或外部硬件電路產生的電信號相對應。它們提供了一種特殊的方式,使處理器轉而去執行正常控制流之外的代碼。
    ⑤中斷是異步的,由硬件隨機產生,在程序執行的任何時候可能出現。異常是同步的,在(特殊的或出錯的)指令執行時由CPU控制單元產生。
    ⑥每個中斷和異常由0~255之間的一個數(8位)來標識,Intel稱其爲中斷向量(vector)。非屏蔽中斷的向量和異常的向量是固定的,可屏蔽中斷的向量可以通過對中斷控制器的編程來改變。

  3. 全局變量和局部變量都保存在哪兒?
    答:全局變量保存在內存的全局存儲區中,佔用靜態的存儲單元;局部變量保存在棧中,只有在所在函數被調用時才動態地爲變量分配存儲單元。
    擴展知識:全局變量、靜態全局變量、靜態局部變量和局部變量的區別
    變量可以分爲:全局變量、靜態全局變量、靜態局部變量和局部變量。
    按存儲區域分,全局變量、靜態全局變量和靜態局部變量都存放在內存的靜態存儲區域,局部變量存放在內存的棧區。
    按作用域分,全局變量在整個工程文件內都有效;靜態全局變量只在定義它的文件內有效;靜態局部變量只在定義它的函數內有效,並且程序僅分配一次內存,函數返回後,該變量不會消失;局部變量在定義它的函數內有效,但是函數返回後失效。
    全局變量(外部變量)的說明之前再冠以static 就構成了靜態的全局變量。全局變量本身就是靜態存儲方式, 靜態全局變量當然也是靜態存儲方式。 這兩者在存儲方式上並無不同。這兩者的區別在於**非靜態全局變量的作用域是整個源程序,當一個源程序由多個源文件組成時,非靜態的全局變量在各個源文件中都是有效的。 而靜態全局變量則限制了其作用域, 即只在定義該變量的源文件內有效, 在同一源程序的其它源文件中不能使用它。**由於靜態全局變量的作用域侷限於一個源文件內,只能爲該源文件內的函數公用, 因此可以避免在其它源文件中引起錯誤。
    從以上分析可以看出,把局部變量改變爲靜態變量後是改變了它的存儲方式即改變了它的生存期。把全局變量改變爲靜態變量後是改變了它的作用域,限制了它的使用範圍
    全局變量和靜態變量如果沒有手工初始化,則由編譯器初始化爲0。局部變量的值不可知。

  4. 介紹進程跟線程,進程之間的通信
    1)首先了解一下進程和線程的區別
    對於進程來說,子進程是父進程的複製品,從父進程那裏獲得父進程的數據空間,堆和棧的複製品
    線程,相對於進程而言,是一個更加接近於執行體的概念,可以和同進程的其他線程之間直接共享數據,而且擁有自己的棧空間,擁有獨立序列。
    共同點: 它們都能提高程序的併發度,提高程序運行效率和響應時間。線程和進程在使用上各有優缺點。 線程執行開銷比較小,但不利於資源的管理和保護,而進程相反。同時,線程適合在SMP機器上運行,而進程可以跨機器遷移
    它們之間根本區別在於:多進程中每個進程有自己的地址空間,線程則共享地址空間。所有其他區別都是因爲這個區別產生的。比如說:

  5. 速度。線程產生的速度快,通訊快,切換快,因爲他們處於同一地址空間。

  6. 線程的資源利用率好。

  7. 線程使用公共變量或者內存的時候需要同步機制,但進程不用。
    而他們通信方式的差異也仍然是由於這個根本原因造成的。
    2)通信方式之間的差異:因爲那個根本原因,實際上只有進程間需要通信同一進程的線程共享地址空間,沒有通信的必要,但要做好同步/互斥,保護共享的全局變量。進程間通信無論是信號,管道pipe還是共享內存都是由操作系統保證的,是系統調用.
    進程間通信:
    ①管道( pipe ):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係。
    ②有名管道 (namedpipe) :有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信。
    ③信號量(semophore ) :信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
    ④消息隊列( messagequeue ) :消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
    ⑤信號 (sinal ) :信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
    ⑥共享內存(shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信。
    ⑦套接字(socket ) :套接口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同設備及其間的進程通信。
    3)線程間的通信方式:
    ①鎖機制:包括互斥鎖、條件變量、讀寫鎖

    • 互斥鎖提供了以排他方式防止數據結構被併發修改的方法。
    • 讀寫鎖允許多個線程同時讀共享數據,而對寫操作是互斥的。
    • 條件變量可以以原子的方式阻塞進程,直到某個特定條件爲真爲止。對條件的測試是在互斥鎖的保護下進行的。條件變量始終與互斥鎖一起使用。
      ②信號量機制(Semaphore):包括無名線程信號量和命名線程信號量
      ③信號機制(Signal):類似進程間的信號處理
      線程間的通信目的主要是用於線程同步,所以線程沒有像進程通信中的用於數據交換的通信機制。
  8. 如何從用戶態到內核態
    a. 系統調用
    這是用戶態進程主動要求切換到內核態的一種方式,用戶態進程通過系統調用申請使用操作系統提供的服務程序完成工作,比如前例中fork()實際上就是執行了一個創建新進程的系統調用。而系統調用的機制其核心還是使用了操作系統爲用戶特別開放的一箇中斷來實現,例如Linux的int 80h中斷。
    b. 異常
    當CPU在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就轉到了內核態,比如缺頁異常。
    c. 外圍設備的中斷
    當外圍設備完成用戶請求的操作後,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那麼這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行後續操作等。
    這3種方式是系統在運行時由用戶態轉到內核態的最主要方式,其中系統調用可以認爲是用戶進程主動發起的,異常和外圍設備中斷則是被動的。

Linux部分

  1. 查看進程用什麼指令(jps、ps)
    答:ps是進程查看命令,可用於查看當前運行的進程信息。
    ps命令的參數:
    A :所有的進程均顯示出來,與 -e 具有同樣的效用;
    -a : 顯示現行終端機下的所有進程,包括其他用戶的進程;
    -u :以用戶爲主的進程狀態 ;
    x :通常與 a 這個參數一起使用,可列出較完整信息。
    【ps aux】該命令用於查看當前所有運行的進程。
    【ps -ef | grep xxx】該命令用於查看進程xxx的相關信息。
    【ps aux】與【ps -ef】的區別:
    【ps aux】 是用BSD的格式來顯示進程
    顯示的項目有:USER , PID , %CPU , %MEM , VSZ , RSS , TTY , STAT , START , TIME , COMMAND
    PS AUX的格式
    【ps -ef】 是用標準的格式顯示進程
    顯示的項目有:UID , PID , PPID , C , STIME , TTY , TIME , CMD
    ps -ef的格式
    jps是java自帶的查看java進程的命令,通過這個命令可以查看當前系統所有運行中的java進程、java包名、jar包名及JVM參數等。
    jsp的參數:
    -q: 只顯示VM 標示,不顯示jar,class, main參數等信息。
    -m: 輸出主函數傳入的參數。
    -l: 輸出應用程序主類完整package名稱或jar完整名稱。
    -v: 列出jvm啓動參數。
    -V: 輸出通過.hotsportrc或-XX:Flags=指定的jvm參數。
    -Joption: 傳遞參數到javac 調用的java lancher。
    jps命令不帶選項的簡單列出了進程的pid及java類名及jar包類型

  2. Linux系統,怎麼找到某個端口運行的程序的絕對地址
    答:分爲三步:
    ①lsof -i:端口 //查看端口被佔用的線程
    ②ps axu |grep pid //根據線程查出具體的應用:
    ③whereis <程序名稱>:查找程序的所在路徑

  3. 軟連接和硬鏈接,linux 目錄默認inode多少
    答:首先介紹inode:文件儲存在硬盤上,硬盤的最小存儲單位叫做“扇區”(Sector)。每個扇區儲存512字節(相當於0.5KB)。操作系統讀取硬盤的時候,不會一個個扇區地讀取,這樣效率太低,而是一次性連續讀取多個扇區,即一次性讀取一個”塊”(block)。這種由多個扇區組成的”塊”,是文件存取的最小單位。”塊”的大小,最常見的是4KB,即連續八個 sector組成一個 block。文件數據都儲存在”塊”中,那麼很顯然,我們還必須找到一個地方儲存文件的元信息,比如文件的創建者、文件的創建日期、文件的大小等等。這種儲存文件元信息的區域就叫做inode,中文譯名爲”索引節點”。每一個文件都有對應的inode,裏面包含了與該文件有關的一些信息。
    inode包含了什麼?
    ①文件的字節數
    ②文件擁有者的User ID
    ③文件的Group ID
    ④文件的讀、寫、執行權限
    ⑤文件的時間戳,共有三個:ctime指inode上一次變動的時間,mtime指文件內容上一次變動的時間,atime指文件上一次打開的時間
    ⑥鏈接數,即有多少文件名指向這個inode
    ⑦文件數據block的位置
    可以用stat命令,查看某個文件的inode信息:

stat test.c

然後重點介紹inode的號碼:每個inode都有一個號碼,操作系統用inode號碼來識別不同的文件。這裏值得重複一遍,Unix/Linux系統內部不使用文件名,而使用inode號碼來識別文件。**對於系統來說,文件名只是inode號碼便於識別的別稱或者綽號。**表面上,用戶通過文件名,打開文件。實際上,系統內部這個過程分成三步:
首先,系統找到這個文件名對應的inode號碼;
其次,通過inode號碼,獲取inode信息;
最後,根據inode信息,找到文件數據所在的block,讀出數據。
使用ls -i命令,可以看到文件名對應的inode號碼:

ls -i example.c

接着是:目錄文件
Unix/Linux系統中,目錄(directory)也是一種文件。打開目錄,實際上就是打開目錄文件。目錄文件的結構非常簡單,就是一系列目錄項(dirent)的列表。每個目錄項,由兩部分組成:所包含文件的文件名,以及該文件名對應的inode號碼。
ls命令只列出目錄文件中的所有文件名:

ls /etc

ls -i命令列出整個目錄文件,即文件名和inode號碼:

ls -i /etc

如果要查看文件的詳細信息,就必須根據inode號碼,訪問inode節點,讀取信息。ls -l命令列出文件的詳細信息。

ls -l /etc

理解了上面這些知識,就能理解目錄的權限。
目錄文件的讀權限(r)和寫權限(w),都是針對目錄文件本身。由於目錄文件內只有文件名和inode號碼,所以如果只有讀權限,只能獲取文件名,無法獲取其他信息,因爲其他信息都儲存在inode節點中,而讀取inode節點內的信息需要目錄文件的執行權限(x)
硬鏈接
一般情況下,**文件名和inode號碼是”一一對應”關係,每個inode號碼對應一個文件名。但是,Unix/Linux系統允許,多個文件名指向同一個inode號碼。**這意味着,可以用不同的文件名訪問同樣的內容;對文件內容進行修改,會影響到所有文件名;但是,刪除一個文件名,不影響另一個文件名的訪問。這種情況就被稱爲”硬鏈接”(hard link)
ln命令可以創建硬鏈接:

ln 源文件 目標文件

運行上面這條命令以後,源文件與目標文件的inode號碼相同,都指向同一個inode
inode信息中有一項叫做”鏈接數”,記錄指向該inode的文件名總數,這時就會增加1。
反過來,刪除一個文件名,就會使得inode節點中的”鏈接數”減1。當這個值減到0,表明沒有文件名指向這個inode,系統就會回收這個inode號碼,以及其所對應block區域。
這裏順便說一下目錄文件的”鏈接數”創建目錄時,默認會生成兩個目錄項:”.”和”…”。前者的inode號碼就是當前目錄的inode號碼,等同於當前目錄的”硬鏈接”;,後者的inode號碼就是當前目錄的父目錄的inode號碼,等同於父目錄的”硬鏈接”。所以,任何一個目錄的”硬鏈接”總數,總是等於2加上它的子目錄總數(含隱藏目錄)
UNIX文件系統提供了一種將不同文件鏈接至同一個文件的機制,我們稱這種機制爲鏈接。它可以使得單個程序對同一文件使用不同的名字。這樣的好處是文件系統只存在一個文件的副本。系統簡單地通過在目錄中建立一個新的登記項來實現這種連接。該登記項具有一個新的文件名和要連接文件的inode號(inode與原文件相同)。不論一個文件有多少硬鏈接,在磁盤上只有一個描述它的inode,只要該文件的鏈接數不爲0,該文件就保持存在。硬鏈接不能對目錄建立硬鏈接!
硬連接是直接建立在節點表上的(inode),建立硬連接指向一個文件的時候,會更新節點表上面的計數值。舉個例子,一個文件被連接了兩次(硬連接),這個文件的計數值是3,而無論通過3個文件名中的任何一個訪問,效果都是完全一樣的,但是如果刪除其中任意一個,都只是把計數值減1,不會刪除實際的內容的,(任何存在的文件本身就算是一個硬連接)只有計數值變成0也就是沒有任何硬連接指向的時候纔會真實的刪除內容。
軟鏈接
除了硬鏈接以外,還有一種特殊情況。文件A和文件B的inode號碼雖然不一樣,但是文件A的內容是文件B的路徑。讀取文件A時,系統會自動將訪問者導向文件B。因此,無論打開哪一個文件,最終讀取的都是文件B。這時,文件A就稱爲文件B的”軟鏈接”(soft link)或者”符號鏈接(symbolic link)。這意味着,文件A依賴於文件B而存在,如果刪除了文件B,打開文件A就會報錯:”No such file or directory”。
這是軟鏈接與硬鏈接最大的不同:文件A指向文件B的文件名,而不是文件B的inode號碼,文件B的inode”鏈接數”不會因此發生變化。
ln -s命令可以創建軟鏈接。

ln -s 源文文件或目錄 目標文件或目錄

我們把符號鏈接稱爲軟鏈接,它是指向另一個文件的特殊文件,這種文件的數據部分僅包含它所要鏈接文件的路徑名。
軟鏈接是爲了克服硬鏈接的不足而引入的,軟鏈接不直接使用inode號作爲文件指針,而是使用文件路徑名作爲指針(軟鏈接:文件名 + 數據部分–>目標文件的路徑名)。
軟件有自己的inode,並在磁盤上有一小片空間存放路徑名。因此,軟鏈接能夠跨文件系統,也可以和目錄鏈接!其二,軟鏈接可以對一個不存在的文件名進行鏈接,但直到這個名字對應的文件被創建後,才能打開其鏈接。
inode的特殊作用
由於inode號碼與文件名分離,這種機制導致了一些Unix/Linux系統特有的現象。

  1. 有時,文件名包含特殊字符,無法正常刪除。這時,直接刪除inode節點,就能起到刪除文件的作用。
  2. 移動文件或重命名文件,只是改變文件名,不影響inode號碼。
  3. 打開一個文件以後,系統就以inode號碼來識別這個文件,不再考慮文件名。因此,通常來說,系統無法從inode號碼得知文件名。
    第3點使得軟件更新變得簡單,可以在不關閉軟件的情況下進行更新,不需要重啓。因爲系統通過inode號碼,識別運行中的文件,不通過文件名。更新的時候,新版文件以同樣的文件名,生成一個新的inode,不會影響到運行中的文件。等到下一次運行這個軟件的時候,文件名就自動指向新版文件,舊版文件的inode則被回收。

其他

  1. 100個石頭,每個人一次可以摸1-5個,甲先摸,問甲有沒有必贏的方法?
    答:這種題目是考慮倍數的問題。每次最多取5個最少1個,這樣的話就考慮每次取6,
    100÷6=16餘4。
    先拿的人拿4個,不論第二個人拿幾個,第一個人把他湊成6個,這樣永遠是第一個人取到最後一個。

  2. 遊戲模型如何確認人身上的膠囊體是否被激光射中?
    答:求大佬解答

  3. 網頁相似性比較
    答:參考https://blog.csdn.net/beta2/article/details/4974221

  4. RPC框架
    答:RPC就是要像調用本地的函數一樣去調遠程函數。在研究RPC前,我們先看看本地調用是怎麼調的。假設我們要調用函數Multiply來計算lvalue * rvalue的結果:

1 int Multiply(int l, int r) {
2    int y = l * r;
3    return y;
4 }
5 
6 int lvalue = 10;
7 int rvalue = 20;
8 int l_times_r = Multiply(lvalue, rvalue);

那麼在第8行時,我們實際上執行了以下操作:
1.將 lvalue 和 rvalue 的值壓棧
2.進入Multiply函數,取出棧中的值10 和 20,將其賦予 l 和 r
3.執行第2行代碼,計算 l * r ,並將結果存在 y
4.將 y 的值壓棧,然後從Multiply返回
5.第8行,從棧中取出返回值 200 ,並賦值給 l_times_r
以上5步就是執行本地調用的過程。(注:以上步驟只是爲了說明原理。事實上編譯器經常會做優化,對於參數和返回值少的情況會直接將其存放在寄存器,而不需要壓棧彈棧的過程,甚至都不需要調用call,而直接做inline操作。僅就原理來說,這5步是沒有問題的。)
遠程過程調用帶來的新問題
在遠程調用時,我們需要執行的函數體是在遠程的機器上的,也就是說,Multiply是在另一個進程中執行的。這就帶來了幾個新問題:
1.Call ID映射。我們怎麼告訴遠程機器我們要調用Multiply,而不是Add或者FooBar呢?在本地調用中,函數體是直接通過函數指針來指定的,我們調用Multiply,編譯器就自動幫我們調用它相應的函數指針。但是在遠程調用中,函數指針是不行的,因爲兩個進程的地址空間是完全不一樣的。所以,在RPC中,所有的函數都必須有自己的一個ID。這個ID在所有進程中都是唯一確定的。客戶端在做遠程過程調用時,必須附上這個ID。然後我們還需要在客戶端和服務端分別維護一個 {函數 <--> Call ID} 的對應表。兩者的表不一定需要完全相同,但相同的函數對應的Call ID必須相同。當客戶端需要進行遠程調用時,它就查一下這個表,找出相應的Call ID,然後把它傳給服務端,服務端也通過查表,來確定客戶端需要調用的函數,然後執行相應函數的代碼
2.序列化和反序列化。客戶端怎麼把參數值傳給遠程的函數呢?在本地調用中,我們只需要把參數壓到棧裏,然後讓函數自己去棧裏讀就行。但是在遠程過程調用時,客戶端跟服務端是不同的進程,不能通過內存來傳遞參數。甚至有時候客戶端和服務端使用的都不是同一種語言(比如服務端用C++,客戶端用Java或者Python)。這時候就需要客戶端把參數先轉成一個字節流,傳給服務端後,再把字節流轉成自己能讀取的格式。這個過程叫序列化和反序列化。同理,從服務端返回的值也需要序列化反序列化的過程。
3.網絡傳輸。遠程調用往往用在網絡上,客戶端和服務端是通過網絡連接的。所有的數據都需要通過網絡傳輸,因此就需要有一個網絡傳輸層。網絡傳輸層需要把Call ID和序列化後的參數字節流傳給服務端,然後再把序列化後的調用結果傳回客戶端。只要能完成這兩者的,都可以作爲傳輸層使用。因此,它所使用的協議其實是不限的,能完成傳輸就行。儘管大部分RPC框架都使用TCP協議,但其實UDP也可以,而gRPC乾脆就用了HTTP2。Java的Netty也屬於這層的東西。有了這三個機制,就能實現RPC了,具體過程如下:。

// Client端 
//    int l_times_r = Call(ServerAddr, Multiply, lvalue, rvalue)
1. 將這個調用映射爲Call ID。這裏假設用最簡單的字符串當Call ID的方法
2. 將Call ID,lvalue和rvalue序列化。可以直接將它們的值以二進制形式打包
3. 把2中得到的數據包發送給ServerAddr,這需要使用網絡傳輸層
4. 等待服務器返回結果
5. 如果服務器調用成功,那麼就將結果反序列化,並賦給l_times_r

// Server端
1. 在本地維護一個Call ID到函數指針的映射call_id_map,可以用std::map<std::string, std::function<>>
2. 等待請求
3. 得到一個請求後,將其數據包反序列化,得到Call ID
4. 通過在call_id_map中查找,得到相應的函數指針
5. 將lvalue和rvalue反序列化後,在本地調用Multiply函數,得到結果
6. 將結果序列化後通過網絡返回給Client

所以要實現一個RPC框架,其實只需要按以上流程實現就基本完成了。其中:
Call ID映射可以直接使用函數字符串,也可以使用整數ID。
映射表一般就是一個哈希表。
序列化反序列化可以自己寫,也可以使用Protobuf或者FlatBuffers之類的。
網絡傳輸庫可以自己寫socket,或者用asio,ZeroMQ,Netty之類。
當然,這裏面還有一些細節可以填充,比如如何處理網絡錯誤,如何防止攻擊,如何做流量控制,等等。但有了以上的架構,這些都可以持續加進去。

  1. Git常用指令
    答:
# 下載一個項目和它的整個代碼歷史
$ git clone [url]

# 顯示當前的Git配置
$ git config --list

# 編輯Git配置文件
$ git config -e [--global]

# 設置提交代碼時的用戶信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"

# 添加指定文件到暫存區
$ git add [file1] [file2] ...

# 添加指定目錄到暫存區,包括子目錄
$ git add [dir]

# 添加當前目錄的所有文件到暫存區
$ git add .

# 提交暫存區到倉庫區
$ git commit -m [message]

# 提交暫存區的指定文件到倉庫區
$ git commit [file1] [file2] ... -m [message]

# 列出所有本地分支
$ git branch

# 列出所有遠程分支
$ git branch -r

# 列出所有本地分支和遠程分支
$ git branch -a

# 新建一個分支,但依然停留在當前分支
$ git branch [branch-name]

# 新建一個分支,並切換到該分支
$ git checkout -b [branch]

# 切換到指定分支,並更新工作區
$ git checkout [branch-name]

# 切換到上一個分支
$ git checkout -

# 顯示有變更的文件
$ git status

# 顯示當前分支的版本歷史
$ git log

# 顯示暫存區和工作區的代碼差異
$ git diff

# 顯示某次提交的元數據和內容變化
$ git show [commit]

# 取回遠程倉庫的變化,並與本地分支合併
$ git pull [remote] [branch]

# 上傳本地指定分支到遠程倉庫
$ git push [remote] [branch]

# 重置暫存區與工作區,與上一次commit保持一致
$ git reset --hard

# 暫時將未提交的變化移除,稍後再移入
$ git stash
$ git stash pop
  1. 服務感知(客戶端如何感知服務端狀態)
    答:客戶端每10s(舉個例子)向服務器端發一個信息,服務端將發這個數據的信息和保存時間存入表中,隨後應用用定時任務不斷的檢查當前時間和剛剛存入的時間的差值:這裏用到了這個函數TIMESTAMPDIFF(SECOND, dataEnterTime,now()),如果這個值大於20s,就判定斷網。其實就是定時任務開啓,不斷掃描最後一次發送時間和現在時間的差值,時間太長的話,那就是斷了,而不是每次都主動向客戶端發信息。

  2. 如果地球自轉速度降低一半,會怎麼樣?
    答:海洋與陸地重新洗牌、時間變“慢”、可怕的慣性、地球會變成完美球體,參考:https://www.zhihu.com/question/280709798

參考資料

  1. https://blog.csdn.net/maochengtao/article/details/42584539
  2. https://blog.csdn.net/aigoogle/article/details/30307485
  3. https://www.cnblogs.com/fensi/p/13221753.html
  4. https://blog.csdn.net/hxqneuq2012/article/details/52709652
  5. https://blog.csdn.net/liyue98/article/details/80112246
  6. https://blog.csdn.net/shanghx_123/article/details/83151064
  7. https://www.zhihu.com/question/25536695
  8. https://www.cnblogs.com/chenwolong/p/GIT.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章