操作系統(tǒng)初識

[toc]

操作系統(tǒng)

熟練使用 Linux 命令行 -> 使用 Linux 進行程序設(shè)計 -> 了解 Linux 內(nèi)核機制 -> 閱讀 Linux 內(nèi)核代碼 -> 實驗定制 Linux 組件 -> 以及最后落到生產(chǎn)實踐上

鼠標(biāo)雙擊會觸發(fā)一個中斷州疾,操作系統(tǒng)里面就是調(diào)用中斷處理函數(shù)祸轮,分析中斷皆的,并執(zhí)行對應(yīng)的程序豌鹤。

在操作系統(tǒng)中捅厂,進程的執(zhí)行需要分配CPU進行執(zhí)行柳恐,也就是按照程序里面的二進制代碼一行一行地執(zhí)行倒槐。眾多進程交替使用CPU惕艳,為了管理進程锈至,就需要一個進程管理子系統(tǒng)晨缴。同樣CPU并發(fā)的運行多個進程,也需要CPU的調(diào)度能力峡捡。

對于 QQ 來講击碗,由于鍵盤閃啊閃的焦點在QQ這個對話框上,因而操作系統(tǒng)知道们拙,這個事件是給這個進程的稍途。QQ的代碼里面肯定有遇到這種事件如何處理的代碼,就會執(zhí)行砚婆。一般是記錄下客戶的輸入械拍,并且告知顯卡驅(qū)動程序,在那個地方畫一個“a”装盯。顯卡畫完了坷虑,客戶看到了,就覺得自己的輸入成功了埂奈。

當(dāng)用戶輸入完畢之后迄损,回車一下,還是會通過鍵盤驅(qū)動程序告訴操作系統(tǒng)账磺,操作系統(tǒng)還是會找到QQ芹敌,QQ會將用戶的輸入發(fā)送到網(wǎng)絡(luò)上痊远。QQ進程是不能直接發(fā)送網(wǎng)絡(luò)包的,需要調(diào)用系統(tǒng)調(diào)用氏捞,內(nèi)核使用網(wǎng)卡驅(qū)動程序進行發(fā)送碧聪。

undefined

Linux 基礎(chǔ)命令

--help
man
passwd
useradd [hikari] 默認就會創(chuàng)建一個同名的組。
rpm -qa 安裝的軟件列表 
rpm -qa | more和rpm -qa | less 們可以將很長的結(jié)果分頁展示出來 z 下一頁 w 上一頁幌衣。jk上一條下一條
rpm -e和dpkg -r矾削。-e 就是 erase,-r 就是 remove

因為 Linux 現(xiàn)在常用的有兩大體系豁护,一個是 CentOS 體系哼凯,一個是 Ubuntu 體系,前者使用 rpm楚里,后者使用 deb断部。CentOS 下面使用rpm -i jdk-XXX_linux-x64_bin.rpm進行安裝,Ubuntu 下面使用dpkg -i jdk-XXX_linux-x64_bin.deb班缎。其中 -i 就是 install 的意思

以上是沒有軟件管家安裝軟件的方式蝴光。后來Linux 也有自己的軟件管家,CentOS 下面是 yum达址,Ubuntu 下面是 apt-get蔑祟。ubuntu 中現(xiàn)在用apt替換了apt-get

在軟件管家中搜索對應(yīng)的內(nèi)容
yum search jdk和apt-cache search jdk
通過軟件管家安裝:
yum install java-11-openjdk.x86_64
apt-get install openjdk-9-jdk來進行安裝
通過軟件管家進行卸載
yum erase java-11-openjdk.x86_64
apt-get purge openjdk-9-jdk

Linux 允許我們配置從哪里下載這些軟件的,地點就在配置文件里面沉唠。對于 CentOS 來講疆虚,配置文件在/etc/yum.repos.d/CentOS-Base.repo

[base]
name=CentOS-$releasever - Base - 163.com
baseurl=http://mirrors.163.com/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7

對于 Ubuntu 來講,配置文件在/etc/apt/sources.list里满葛。

deb http://mirrors.163.com/ubuntu/ xenial main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ xenial-security main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ xenial-updates main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ xenial-proposed main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ xenial-backports main restricted universe multiverse

其實無論是先下載再安裝径簿,還是通過軟件管家進行安裝,都是下載一些文件嘀韧,然后將這些文件放在某個路徑下篇亭,然后在相應(yīng)的配置文件中配置一下 在 Linux 里面會放的散一點。例如锄贷,主執(zhí)行文件會放在 /usr/bin 或者 /usr/sbin 下面译蒂,其他的庫文件會放在 /var 下面,配置文件會放在 /etc 下面谊却。

也可以選擇直接將安裝好的路徑直接下載下來柔昼,然后解壓縮成為一個整的路徑。如:一個jdk-XXX_linux-x64_bin.tar.gz這樣的文件因惭。需要注意的是岳锁,通過這種方式下載的內(nèi)容需要在環(huán)境變量進行相應(yīng)的配置。

先通過wget命令下載上對應(yīng)的壓縮包蹦魔。
wget jdk-XXX_linux-x64_bin.tar.gz
對應(yīng)著tar.gz這種格式的壓縮包激率,直接通過tar命令進行解壓
tar xvzf jdk-XXX_linux-x64_bin.tar.gz

配置環(huán)境變量咳燕。export 命令僅在當(dāng)前命令行的會話中管用,一旦退出重新登錄進來乒躺,就不管用了

export JAVA_HOME=/root/jdk-XXX_linux-x64
export PATH=$JAVA_HOME/bin:$PATH

在當(dāng)前用戶的默認工作目錄招盲,例如 /root 或者 /home/cliu8 下面,有一個.bashrc 文件嘉冒,這個文件是以點開頭的曹货,這個文件默認看不到,需要 ls -la 才能看到讳推,a 就是 all顶籽。每次登錄的時候,這個文件都會運行银觅,因而把它放在這里礼饱。這樣登錄進來就會自動執(zhí)行。當(dāng)然也可以通過 source .bashrc 手動執(zhí)行究驴。

Linux 通過./filename運行這個程序镊绪。如果放在 PATH 里設(shè)置的路徑下面,就不用./ 了洒忧,直接輸入文件名就可以運行了蝴韭,Linux 會幫你找。比如 mysql

交互命令行退出熙侍,程序還在運行:

  • 使用nohup命令榄鉴。這個命令的意思是 no hang up(不掛起)。
  • 這個時候核行,程序不能霸占交互命令行牢硅,而是應(yīng)該在后臺運行蹬耘。最后加一個 &芝雪,就表示后臺運行
nohup &
-----
最終命令的一般形式:nohup command >out.file 2>&1 &

這里面,“1”表示文件描述符 1综苔,表示標(biāo)準(zhǔn)輸出惩系,“2”表示文件描述符 2,意思是標(biāo)準(zhǔn)錯誤輸出如筛,“2>&1”表示標(biāo)準(zhǔn)輸出和錯誤輸出合并了堡牡。合并到到 out.file 這個文件里了。

啟動的程序如何退出

ps -ef |grep 關(guān)鍵字  |awk '{print $2}'|xargs kill -9
  • ps -ef 可以單獨執(zhí)行杨刨,列出所有正在運行的程序
  • grep 通過關(guān)鍵字找到剛才啟動的程序晤柄。
  • awk 工具可以很靈活地對文本進行處理,這里的 awk '{print $2}'是指第二列的內(nèi)容妖胀,是運行的程序 ID
  • 可以通過 xargs 傳遞給 kill -9芥颈,也就是發(fā)給這個運行的程序一個信號惠勒,讓它關(guān)閉

如果你已經(jīng)知道運行的程序 ID,可以直接使用 kill 關(guān)閉運行的程序

Linux 中的程序也可以以服務(wù)的方式運行 例如MySQL

Ubuntu:
啟動 MySQL:systemctl start mysql
設(shè)置開機啟動:systemctl enable mysql

Ubuntu下之所以成為服務(wù)并且能夠開機啟動爬坑,是因為在 /lib/systemd/system 目錄下會創(chuàng)建一個 XXX.service 的配置文件纠屋,里面定義了如何啟動、如何關(guān)閉盾计。

CentOS - MariaDB
進行安裝:yum install mariadb-server mariadb
啟動:systemctl start mariadb
設(shè)置開機啟動:systemctl enable mariadb

Linux 掛機和重啟

現(xiàn)在就關(guān)機:shutdown -h now
重啟:reboot

.bash_profile是系統(tǒng)配置信息存儲文件售担,寫在里面的系統(tǒng)變量是所有用戶共用的,而.bashrc是個人的配置信息存儲文件署辉,只是單用戶有效族铆。也就是說,配置了.bashrc后切換用戶可能需要重新配置系統(tǒng)變量哭尝。

undefined

系統(tǒng)調(diào)用

創(chuàng)建進程的系統(tǒng)調(diào)用叫fork骑素。在Linux里,要創(chuàng)建一個新的進程刚夺,需要一個老的進程調(diào)用fork來實現(xiàn)献丑,其中老的進程叫作父進程,新的進程叫作子進程侠姑。由于在Linux中創(chuàng)建一個進程需要協(xié)調(diào)的系統(tǒng)資源很多创橄,步驟也很繁瑣。所以Linux干脆直接在原來父進程的基礎(chǔ)上fork一份子進程出來即可莽红,原模原樣妥畏。這樣簡單也快速。

當(dāng)父進程調(diào)用 fork 創(chuàng)建子進程的時候安吁,子進程將各個為父進程創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)也全部拷貝了一份醉蚁,甚至連程序代碼也是拷貝過來的。按理說鬼店,如果不進行特殊的處理网棍,父進程和子進程都按相同的程序代碼進行下去,這樣就沒有意義了妇智。所以滥玷,我們往往會這樣處理:對于 fork 系統(tǒng)調(diào)用的返回值,如果當(dāng)前進程是子進程巍棱,就返回 0惑畴;如果當(dāng)前進程是父進程,就返回子進程的進程號航徙。這樣首先在返回值這里就有了一個區(qū)分如贷,然后如果是父進程,還接著做原來應(yīng)該做的事情;如果是子進程杠袱,需要請求另一個系統(tǒng)調(diào)用execve來執(zhí)行另一個程序泻红,這個時候,子進程和父進程就徹底分道揚鑣了霞掺,也就產(chǎn)生了一個分支(fork)了谊路。

對于Linux系統(tǒng),啟動的時候會先創(chuàng)建一個所有用戶進程的“祖宗進程”菩彬。 父進程可以通過系統(tǒng)調(diào)用waitpid缠劝。父進程可以調(diào)用它,將子進程的進程號作為參數(shù)傳給它骗灶,這樣父進程就知道子進程運行完了沒有惨恭,成功與否。

各進程有獨立的內(nèi)存空間耙旦,對于進程的內(nèi)存空間來講脱羡,放程序代碼的這部分,稱為代碼段免都。放進程運行中產(chǎn)生數(shù)據(jù)的這部分锉罐,稱為數(shù)據(jù)段。其中局部變量的部分绕娘,在當(dāng)前函數(shù)執(zhí)行的時候起作用脓规,當(dāng)進入另一個函數(shù)時,這個變量就釋放了险领;也有動態(tài)分配的侨舆,會較長時間保存,指明才銷毀的绢陌,這部分稱為挨下。

一個進程在32位的計算機中最大內(nèi)存空間是4G。不會給所有進程在創(chuàng)建的時候就分配好這么大的內(nèi)存脐湾,都是在內(nèi)存使用的過程中才進行增量創(chuàng)建的臭笆。并且進程只有真的寫入數(shù)據(jù)的時候,發(fā)現(xiàn)沒有對應(yīng)物理內(nèi)存沥割,才會觸發(fā)一個中斷耗啦,現(xiàn)分配物理內(nèi)存凿菩。

堆中分配內(nèi)存的系統(tǒng)調(diào)用:

  • brk:當(dāng)分配的內(nèi)存數(shù)量比較小的時候机杜,使用 brk,會和原來的堆的數(shù)據(jù)連在一起
  • mmap:當(dāng)分配的內(nèi)存數(shù)量比較大的時候衅谷,使用 mmap椒拗,會重新劃分一塊區(qū)域

文件操作的6個系統(tǒng)調(diào)用:

  • open,close:對于已經(jīng)有的文件,可以使用open打開這個文件蚀苛,close關(guān)閉這個文件在验;
  • creat:對于沒有的文件,可以使用creat創(chuàng)建文件
  • lseek:打開文件以后堵未,可以使用lseek跳到文件的某個位置腋舌;
  • read,write:可以對文件的內(nèi)容進行讀寫渗蟹,讀的系統(tǒng)調(diào)用是read块饺,寫是write

Linux 系統(tǒng)下,萬物皆文件吁津。每個文件读第,Linux都會分配一個文件描述符们妥,這是一個整數(shù)。有了這個文件描述符淮腾,就可以使用系統(tǒng)調(diào)用,切入進程屉佳,查看或者干預(yù)進程運行的方方面面谷朝。

DC1931E4-7BA3-48D1-8E83-22DFBDBC728D.png

信號

信號有以下幾種:

  • 在執(zhí)行一個程序的時候,在鍵盤輸入“CTRL+C”武花,這就是中斷的信號徘禁,正在執(zhí)行的命令就會中止退出;
  • 硬件故障髓堪,設(shè)備出了問題送朱,通知操作系統(tǒng);
  • 用戶進程通過kill函數(shù)干旁,將一個用戶信號發(fā)送給另一個進程驶沼。

每種信號都定義了默認的動作,例如硬件故障争群,默認終止回怜;也可以提供信號處理函數(shù),可以通過sigaction系統(tǒng)調(diào)用换薄,注冊一個信號處理函數(shù)玉雾。

進程間通信

  • 消息隊列:進程間交互的數(shù)據(jù)不大的時候可以通過隊列的方式進行數(shù)據(jù)交換。
  • 共享內(nèi)存:當(dāng)兩個進程間互相通信的內(nèi)容較多的時候轻要,可以使用共享內(nèi)存的方式复旬。這樣數(shù)據(jù)就不需要進行拷貝了。共享內(nèi)存間多進程訪問修改的時候就會有競爭的問題冲泥,通過信號量Semaphore的機制來保證驹碍。
    • 信號量Semaphore:對于只允許一個人訪問的情況壁涎,將信號量設(shè)為1。先調(diào)用sem_wait志秃。如果這時候沒有人訪問怔球,則占用這個信號量,他就可以開始訪問了浮还,信號量的值此時為0竟坛。如果這個時候另一個人要訪問,也會調(diào)用sem_wait钧舌。由于前一個人已經(jīng)在訪問了流码,所以后面這個人就必須等待上一個人訪問完之后才能訪問。當(dāng)上一個人訪問完畢后延刘,會調(diào)用sem_post將信號量釋放漫试,于是下一個人等待結(jié)束,可以訪問這個資源了碘赖。
      -跨網(wǎng)絡(luò)通信:多機之間進程通信需要涉及到網(wǎng)絡(luò)通信驾荣,就需要遵循相同的網(wǎng)絡(luò)協(xié)議(TCP/IP 網(wǎng)絡(luò)協(xié)議棧)。Linux內(nèi)核中有對于網(wǎng)絡(luò)協(xié)議的實現(xiàn)普泡。網(wǎng)絡(luò)服務(wù)是通過套接字Socket來提供的播掷,可以看成兩臺物理機的插槽。通過網(wǎng)線來接通兩機之間的電信號來進行通信撼班。通信前雙方都需要對接口的通信規(guī)則來進行定義歧匈,即雙方需要創(chuàng)建一個Socket。我們可以通過Socket系統(tǒng)調(diào)用建立一個Socket砰嘁。Socket也是一個文件件炉,也有一個文件描述符,也可以通過讀寫函數(shù)進行通信矮湘。

Glibc

雖然Linux提供了有如上這么多甚至更多的系統(tǒng)調(diào)用斟冕,但是對于開發(fā)來說任然不是很友好。調(diào)用起來還是會有難度缅阳,所以在Linux之上磕蛇,有了中介,Glibc十办。Glibc最重要的是封裝了操作系統(tǒng)提供的系統(tǒng)服務(wù)秀撇,即系統(tǒng)調(diào)用的封裝。 有些系統(tǒng)調(diào)用太零碎了向族,整合起來可以完成一個更有效的功能呵燕。

undefined

x86架構(gòu)

CPU

  • 運算單元:只管算,例如做加法炸枣、做位移等虏等。但是弄唧,它不知道應(yīng)該算哪些數(shù)據(jù)适肠,運算結(jié)果應(yīng)該放在哪里霍衫。運算單元計算的數(shù)據(jù)如果每次都要經(jīng)過總線,到內(nèi)存里面現(xiàn)拿侯养,這樣就太慢了敦跌,所以就有了數(shù)據(jù)單元(寄存器)
  • 數(shù)據(jù)單元:數(shù)據(jù)單元包括CPU內(nèi)部的多級緩存緩存寄存器組逛揩,空間很小柠傍,但是速度飛快,可以暫時存放數(shù)據(jù)和運算結(jié)果辩稽。有了放待運算數(shù)據(jù)的地方惧笛,也有了算的地方,還需要有個指揮調(diào)度的邏輯單元逞泄,這就是控制單元患整。
  • 控制單元:控制單元是一個統(tǒng)一的指揮中心,它可以獲得下一條指令喷众,然后執(zhí)行這條指令各谚。這個指令會指導(dǎo)運算單元取出數(shù)據(jù)單元中的某幾個數(shù)據(jù),計算出個結(jié)果到千,然后放在數(shù)據(jù)單元的某個地方昌渤。控制指令從哪里獲取憔四,運算完填入到哪里膀息。

CPU 的控制單元里面,有一個指令指針寄存器了赵,它里面存放的是下一條指令在內(nèi)存中的地址履婉。控制單元會不停地將代碼段的指令拿進來斟览,先放入指令寄存器毁腿。然后交給運算單元去執(zhí)行。CPU和內(nèi)存來來回回傳數(shù)據(jù)苛茂,靠的都是總線已烤。總線有地址總線妓羊,數(shù)據(jù)總線兩類胯究。

CPU 里有兩個寄存器,專門保存當(dāng)前處理進程的代碼段的起始地址躁绸,以及數(shù)據(jù)段的起始地址裕循。這里面寫的都是進程 A臣嚣,那當(dāng)前執(zhí)行的就是進程 A 的指令,等切換成進程 B剥哑,就會執(zhí)行 B 的指令了硅则,這個過程叫作進程切換。

CPU在運算的時候為了暫存數(shù)據(jù)株婴,以8086為例有8個16位的通用寄存器怎虫。也就是CPU的數(shù)據(jù)單元,分別是AX困介、BX大审、CX、DX座哩、SP徒扶、BP、SI根穷、DI姜骡。這些寄存器主要用于在計算過程中暫存數(shù)據(jù)。這些寄存器比較靈活缠诅,其中 AX溶浴、BX、CX管引、DX 可以分成兩個 8 位的寄存器來使用士败,分別是 AH、AL褥伴、BH谅将、BL、CH重慢、CL饥臂、DH、DL似踱。(H代表高位隅熙,L代表低位的意思。)這樣核芽,比較長的數(shù)據(jù)也能暫存囚戚,比較短的數(shù)據(jù)也能暫存。

IP 寄存器就是指令指針寄存器轧简,指向代碼段中下一條指令的內(nèi)存地址驰坊。為了指向不同進程的地址空間,有四個 16 位的段寄存器哮独,分別是 CS拳芙、DS察藐、SS、ES舟扎。

  • CS:代碼段寄存器分飞,通過它可以找到代碼在內(nèi)存中的位置
  • DS:數(shù)據(jù)段的寄存器,通過它可以找到數(shù)據(jù)在內(nèi)存中的位置
  • SS:棧寄存器浆竭,凡是與函數(shù)調(diào)用相關(guān)的操作浸须,都與棧緊密相關(guān)惨寿。

如果運算中需要加載內(nèi)存中的數(shù)據(jù)邦泄,需要通過DS找到內(nèi)存中的數(shù)據(jù),加載到通用寄存器中裂垦。對于一個段顺囊,有一個起始的地址,而段內(nèi)的具體位置蕉拢,稱為偏移量特碳。在CS和DS中都存放著一個段的起始地址。代碼段的偏移量在IP寄存器中晕换,數(shù)據(jù)段的偏移量會放在通用寄存器中午乓。

需要注意的就是在x86剛興起的時代,CS和DS都是16位的闸准,也就是說起始地址都是16位的益愈。IP寄存器和通用寄存器也都是16位的,即偏移量也是 16 位的夷家,但是8086處理器的尋址目標(biāo)是1M大的內(nèi)存空間蒸其。所以它的的地址總線地址是20位。借助段加偏移的方式就能成功湊夠20位的數(shù)據(jù)地址库快。方法:起始地址 *16+ 偏移量摸袁,也就是把 CS 和 DS 中的值左移 4 位,變成 20 位的义屏,然后加上 16 位的偏移量靠汁。

后來32位的計算機問世,在32位處理器中有32根地址總線闽铐,處理器能訪問的內(nèi)存空間2^32=4G蝶怔。原來的8個16位的通用寄存器變成了8個32位的通用寄存器,同時為了向下兼容阳啥,依然保留16位的和8位的使用方式添谊。但過去的段寄存器CS、SS察迟、DS斩狱、ES耳高,不再是段的起始地址,段的起始地址存儲于內(nèi)存中的表格中所踊。表格中的每一項是段描述符泌枪,這里面才是真正的段的起始地址。段寄存器里面保存的是表格中的哪一項秕岛,稱為選擇子碌燕。

這樣,將一個從段寄存器直接拿到的段起始地址继薛,就變成了先間接地從段寄存器找到表格中的一項修壕,再從表格中的一項中拿到段起始地址。

undefined

這兩種模式遏考,前一種稱為實模式慈鸠,后一種模式稱為保護模式。當(dāng)系統(tǒng)剛剛啟動的時候灌具,CPU是處于實模式的青团,這個時候和原來的模式是兼容的,當(dāng)需要更多內(nèi)存的時候咖楣,你可以遵循一定的規(guī)則督笆,進行一系列的操作,然后切換到保護模式诱贿,就能夠用到 32 位 CPU 更強大的能力娃肿。此時不能無縫兼容,但是通過切換模式兼容瘪松。

BIOS

系統(tǒng)剛啟動時咸作,在 x86 系統(tǒng)中,將1M空間最上面的0xF0000到0xFFFFF這64K映射給ROM宵睦,也就是說记罚,到這部分地址訪問的時候,會訪問ROM壳嚎。當(dāng)電腦剛加電的時候桐智,主板通電,處理器會做一些重置的工作烟馅,將 CS 設(shè)置為 0xFFFF说庭,將 IP 設(shè)置為 0x0000,所以第一條指令就會指向 0xFFFF0郑趁,正是在 ROM 的范圍內(nèi)刊驴。在這里,有一個 JMP 命令會跳到 ROM 中做初始化工作的代碼,于是捆憎,BIOS 開始進行初始化的工作舅柜。

undefined

初始化工作包括:

  • 檢查一下系統(tǒng)的硬件
  • 要建立一個中斷向量表和中斷服務(wù)程序 -> 服務(wù)于后續(xù)的鼠標(biāo)鍵盤
  • 內(nèi)存空間映射顯存的空間 -> 在顯示器上顯示一些字符
  • 查看BIOS中的啟動盤選項,找到啟動盤躲惰。執(zhí)行后續(xù)加載操作系統(tǒng)的預(yù)處理代碼

基本上BIOS階段的初始化工作做完致份,系統(tǒng)就會從實模式切換到保護模式中。切換到保護模式開始進行內(nèi)存的訪問方式的配置础拨,建立分段分頁氮块,打開32位地址線,內(nèi)存分塊等诡宗。這一系列初始化工作完成滔蝉,會到選擇操作系統(tǒng)的列表頁面,最后啟動內(nèi)核僚焦。

內(nèi)核初始化

內(nèi)核初始化過程:

  • 進程管理模塊:系統(tǒng)創(chuàng)建第一個進程锰提,0號進程曙痘,也是唯一一個沒有通過 fork 產(chǎn)生的進程芳悲,是進程列表的第一個。
  • 中斷處理模塊:里面設(shè)置了很多中斷門边坤,用于處理各種中斷名扛,系統(tǒng)調(diào)用也是通過發(fā)送中斷的方式進行的。
  • 內(nèi)存管理模塊
  • 初始化進程調(diào)度模塊
  • 虛擬文件系統(tǒng)
  • 以及一些小的其他的初始化模塊茧痒,由無實際意義的0號進程fork而來吨艇。
    • 初始化 1 號進程:用戶進程
    • 創(chuàng)建 2 號進程:內(nèi)核態(tài)的進程

用戶進程承載用戶態(tài)的所有工作牵舱,當(dāng)一個用戶態(tài)的程序運行到一半,要訪問一個核心資源,例如訪問網(wǎng)卡發(fā)一個網(wǎng)絡(luò)包一忱,就需要暫停當(dāng)前的運行,調(diào)用系統(tǒng)調(diào)用晨仑,接下來就輪到內(nèi)核中的代碼運行了藕帜。內(nèi)核將從系統(tǒng)調(diào)用傳過來的包,在網(wǎng)卡上排隊樱调,輪到的時候就發(fā)送约素。發(fā)送完了,系統(tǒng)調(diào)用就結(jié)束了笆凌,返回用戶態(tài)圣猎,讓暫停運行的程序接著運行。進程暫停的那一刻乞而,要把當(dāng)時 CPU的寄存器的值全部暫存到一個地方送悔,這個地方可以放在進程管理系統(tǒng)很容易獲取的地方。當(dāng)系統(tǒng)調(diào)用完畢,返回的時候欠啤,再從這個地方將寄存器的值恢復(fù)回去鳍怨,就能接著運行了

過程如下:用戶態(tài) - 系統(tǒng)調(diào)用 - 保存寄存器 - 內(nèi)核態(tài)執(zhí)行系統(tǒng)調(diào)用(寄存器的使用) - 恢復(fù)寄存器 - 返回用戶態(tài),然后接著原步驟運行跪妥。

從內(nèi)核態(tài)來看鞋喇,無論是進程,還是線程眉撵,我們都可以統(tǒng)稱為任務(wù)侦香,都使用相同的數(shù)據(jù)結(jié)構(gòu)Task,平放在同一個鏈表中纽疟。

系統(tǒng)調(diào)用

32 位系統(tǒng)調(diào)用過程:

  1. 將請求參數(shù)放在寄存器
  2. 根據(jù)系統(tǒng)調(diào)用的名稱罐韩,得到系統(tǒng)調(diào)用號,放在寄存器 eax 里面
  3. 執(zhí)行ENTER_KERNEL污朽,觸發(fā)一個軟中斷散吵,通過它就可以陷入(trap)內(nèi)核。
  4. 軟終端的陷入門接手蟆肆,保存當(dāng)前用戶態(tài)的寄存器矾睦。保存所有的寄存器
  5. 將系統(tǒng)調(diào)用號從 eax 里面取出來,然后根據(jù)系統(tǒng)調(diào)用號炎功,在系統(tǒng)調(diào)用表中找到相應(yīng)的函數(shù)進行調(diào)用枚冗。并將寄存器中保存的參數(shù)取出來,作為函數(shù)參數(shù)(第1步存入的參數(shù))蛇损。
  6. 系統(tǒng)調(diào)用結(jié)束赁温,執(zhí)行INTERRUPT_RETURN,中斷返回淤齐。將原來用戶態(tài)保存的現(xiàn)場恢復(fù)回來股囊,包含代碼段、指令指針寄存器等更啄。這時候用戶態(tài)進程恢復(fù)執(zhí)行稚疹。
undefined

64 位系統(tǒng)調(diào)用過程:

  1. 將請求參數(shù)放在寄存器,有區(qū)別與32位系統(tǒng)調(diào)用锈死,(特殊模塊寄存器)
  2. 根據(jù)系統(tǒng)調(diào)用的名稱贫堰,得到系統(tǒng)調(diào)用號,放在寄存器 eax 里面
  3. 真正的系統(tǒng)調(diào)用待牵,非中斷方式其屏。通過syscall指令。
  4. 執(zhí)行entry_SYSCALL_64缨该,保存了很多寄存器偎行,例如用戶態(tài)的代碼段、數(shù)據(jù)段、保存參數(shù)的寄存器蛤袒。
  5. 從 rax 里面拿出系統(tǒng)調(diào)用號熄云,然后根據(jù)系統(tǒng)調(diào)用號,在系統(tǒng)調(diào)用表中找到相應(yīng)的函數(shù)進行調(diào)用妙真。并將寄存器中保存的參數(shù)取出來缴允,作為函數(shù)參數(shù)。
  6. 系統(tǒng)調(diào)用結(jié)束珍德,執(zhí)行USERGS_SYSRET64练般。
undefined

無論32位還是64位的系統(tǒng)調(diào)用過程都用到了系統(tǒng)調(diào)用表。32位和64位系統(tǒng)調(diào)用表的區(qū)別

  • 32:系統(tǒng)調(diào)用表定義在 arch/x86/entry/syscalls/syscall_32.tbl
  • 64:系統(tǒng)調(diào)用表定義在 arch/x86/entry/syscalls/syscall_64.tbl
32和64的系統(tǒng)調(diào)用函數(shù) open 的定義區(qū)別:
      系統(tǒng)調(diào)用號          系統(tǒng)調(diào)用的名字   系統(tǒng)調(diào)用在內(nèi)核的實現(xiàn)函數(shù)
32    5          i386     open             sys_open                  compat_sys_open
64    2          common   open             sys_open

進程

在Linux下锈候,處理器能執(zhí)行的二進制的程序格式是ELF(可執(zhí)行與可鏈接格式)薄料。ELF二進制文件格式:

undefined
  • .text:放編譯好的二進制可執(zhí)行代碼
  • .data:已經(jīng)初始化好的全局變量
  • .rodata:只讀數(shù)據(jù),例如字符串常量泵琳、const 的變量
  • .bss:未初始化全局變量摄职,運行時會置 0
  • .symtab:符號表,記錄的則是函數(shù)和變量
  • .strtab:字符串表获列、字符串常量和變量名

局部變量是放在棧里面的谷市,是程序運行過程中隨時分配空間,隨時釋放的蛛倦,所以不會在程序的二進制文件中體現(xiàn)歌懒,由于二進制文件還沒啟動,所以二進制文件中只需要考慮在哪里保存全局變量溯壶。

  • 靜態(tài)鏈接庫:靜態(tài)鏈接庫被代碼鏈接進去,代碼和變量都合并了甫男,因而程序運行的時候且改,就不依賴于這個庫是否存在。但是這樣有一個缺點板驳,就是相同的代碼段又跛,如果被多個程序使用的話,在內(nèi)存里面就有多份若治,而且一旦靜態(tài)鏈接庫更新了慨蓝,如果二進制執(zhí)行文件不重新編譯,也不隨著更新端幼。
  • 動態(tài)鏈接庫:當(dāng)一個動態(tài)鏈接庫被鏈接到一個程序文件中的時候礼烈,最后的程序文件并不包括動態(tài)鏈接庫中的代碼,而僅僅包括對動態(tài)鏈接庫的引用婆跑,并且不保存動態(tài)鏈接庫的全路徑此熬,僅僅保存動態(tài)鏈接庫的名稱。當(dāng)運行這個程序的時候,首先尋找動態(tài)鏈接庫犀忱,然后加載它募谎。

系統(tǒng)啟動之后,init 進程會啟動很多的daemon進程阴汇,為系統(tǒng)運行提供服務(wù)数冬。,然后就是啟動getty搀庶,讓用戶登錄吉执,登錄后運行 shell,用戶啟動的進程都是通過 shell 運行的地来,從而形成了一棵進程樹戳玫。


[root@deployer ~]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0  2018 ?        00:00:29 /usr/lib/systemd/systemd --system --deserialize 21
root         2     0  0  2018 ?        00:00:00 [kthreadd]
root         3     2  0  2018 ?        00:00:00 [ksoftirqd/0]
root         5     2  0  2018 ?        00:00:00 [kworker/0:0H]
root         9     2  0  2018 ?        00:00:40 [rcu_sched]
......
root       337     2  0  2018 ?        00:00:01 [kworker/3:1H]
root       380     1  0  2018 ?        00:00:00 /usr/lib/systemd/systemd-udevd
root       415     1  0  2018 ?        00:00:01 /sbin/auditd
root       498     1  0  2018 ?        00:00:03 /usr/lib/systemd/systemd-logind
......
root       852     1  0  2018 ?        00:06:25 /usr/sbin/rsyslogd -n
root      2580     1  0  2018 ?        00:00:00 /usr/sbin/sshd -D
root     29058     2  0 Jan03 ?        00:00:01 [kworker/1:2]
root     29672     2  0 Jan04 ?        00:00:09 [kworker/2:1]
root     30467     1  0 Jan06 ?        00:00:00 /usr/sbin/crond -n
root     31574     2  0 Jan08 ?        00:00:01 [kworker/u128:2]
......
root     32792  2580  0 Jan10 ?        00:00:00 sshd: root@pts/0
root     32794 32792  0 Jan10 pts/0    00:00:00 -bash
root     32901 32794  0 00:01 pts/0    00:00:00 ps -ef

PID 1 的進程就是init 進程 systemd,PID2的進程是內(nèi)核線程kthreadd未斑,其中用戶態(tài)的不帶中括號咕宿,內(nèi)核態(tài)的帶中括號。所有帶中括號的內(nèi)核態(tài)的進程蜡秽,祖先都是2號進程府阀。而用戶態(tài)的進程,祖先都是1號進程芽突。tty那一列试浙,是問號的,說明不是前臺啟動的寞蚌,一般都是后臺的服務(wù)田巴。

首先通過文件編譯過程,生成 so 文件和可執(zhí)行文件挟秤,放在硬盤上壹哺。用戶態(tài)的進程 A 執(zhí)行 fork,創(chuàng)建進程 B艘刚,在進程 B 的處理邏輯中管宵,執(zhí)行 exec 系列系統(tǒng)調(diào)用。這個系統(tǒng)調(diào)用會通過load_elf_binary方法攀甚,將剛才生成的可執(zhí)行文件箩朴,加載到進程 B 的內(nèi)存中執(zhí)行

undefined

有的進程只有一個線程秋度,有的進程有多個線程炸庞,它們都需要由內(nèi)核分配 CPU 來干活。在 Linux 里面静陈,無論是進程燕雁,還是線程诞丽,到了內(nèi)核里面,統(tǒng)一都叫任務(wù)(Task)拐格,由一個統(tǒng)一的結(jié)構(gòu) task_struct 進行管理僧免。

undefined

Linux中各進程的執(zhí)行狀態(tài):

undefined

進程的狀態(tài)切換往往涉及調(diào)度,進程常見狀態(tài):

  • TASK_RUNNING:表示進程在時刻準(zhǔn)備運行的狀態(tài)捏浊,并非正在運行懂衩。當(dāng)處于這個狀態(tài)的進程獲得時間片的時候,就是在運行中金踪;如果沒有獲得時間片浊洞,就說明它被其他進程搶占了,在等待再次分配時間片胡岔。 在運行中的進程法希,一旦要進行一些 I/O 操作,需要等待 I/O 完畢靶瘸,這個時候會釋放 CPU苫亦,進入睡眠狀態(tài)。
  • TASK_INTERRUPTIBLE:可中斷的睡眠狀態(tài)怨咪,淺睡眠的狀態(tài)屋剑。雖然在睡眠,等待I/O完成诗眨,但是這個時候一個信號來的時候唉匾,進程還是要被喚醒,不需要死等IO操作完成匠楚。喚醒后巍膘,進行信號處理,怎么處理取決于信號處理函數(shù)油啤。像IO正常結(jié)束即程序原樣繼續(xù)運行典徘。
  • TASK_UNINTERRUPTIBLE:不可中斷的睡眠狀態(tài),深度睡眠狀態(tài)益咬。不可被信號喚醒,只能死等 I/O 操作完成帜平。一旦 I/O 操作因為特殊原因不能完成幽告,這個時候,誰也叫不醒這個進程了裆甩。即便kill也不行冗锁,因為kill本身就屬于一個信號。 除非重啟電腦嗤栓,所以一般不建議設(shè)置成該狀態(tài)冻河。
  • TASK_KILLABLE:與TASK_UNINTERRUPTIBLE原理類似箍邮,但能響應(yīng)致命信號。
  • TASK_STOPPED:是在進程接收到 SIGSTOP叨叙、SIGTTIN锭弊、SIGTSTP 或者 SIGTTOU 信號之后進入該狀態(tài)。
  • EXIT_ZOMBIE:一旦一個進程要結(jié)束擂错,先進入的是 EXIT_ZOMBIE 狀態(tài)味滞,但是這個時候它的父進程還沒有使用 wait() 等系統(tǒng)調(diào)用來獲知它的終止信息,此時進程就成了僵尸進程钮呀。
  • EXIT_DEAD:是進程的最終狀態(tài)剑鞍。

每個進程都有自己獨立的虛擬內(nèi)存空間,這需要有一個數(shù)據(jù)結(jié)構(gòu)來表示爽醋,就是mm_struct蚁署。每個進程有一個文件系統(tǒng)的數(shù)據(jù)結(jié)構(gòu),還有一個打開文件的數(shù)據(jù)結(jié)構(gòu)蚂四。

在進程的內(nèi)存空間里面光戈,棧是一個從高地址到低地址,往下增長的結(jié)構(gòu)证杭,也就是上面是棧底田度,下面是棧頂,入棧和出棧的操作都是從下面的棧頂開始的解愤。

undefined

64位內(nèi)核棧區(qū)別于32位內(nèi)核棧镇饺,每個CPU運行的task_struct不再通過thread_info獲取,而是直接放在 Per CPU 變量里面了送讲。多核情況下奸笤,CPU是同時運行的,但是它們共同使用其他的硬件資源的時候哼鬓,需要解決多個 CPU 之間的同步問題监右。Per CPU 變量就是為每個CPU構(gòu)造一個變量的副本,這樣多個CPU各自操作自己的副本异希,互不干涉健盒。如果進程運行時想要知道當(dāng)前的task_struct在何處時就不需要再通過調(diào)用thread_info結(jié)構(gòu)來找了,各CPU的Pre CPU都有保存該信息称簿。也即不需要CPU間多核情況并行運行時的同步問題了扣癣。

進程的創(chuàng)建

之前闡述過每個進程在操作系統(tǒng)中都是通過fork函數(shù)創(chuàng)建的,fork系統(tǒng)調(diào)用函數(shù)首先做的就是corp_process憨降。把父進程的進程變量和數(shù)據(jù)結(jié)構(gòu)和文件系統(tǒng)相關(guān)變量還有信號相關(guān)的變量等都重新初始化一遍父虑。

  1. 進程打開的文件信息。這些信息用一個結(jié)構(gòu) files_struct 來維護授药,每個打開的文件都有一個文件描述符士嚎。子進程fork的時候復(fù)制的也主要是這些結(jié)構(gòu)呜魄。
  2. 進程的信號相關(guān)變量的數(shù)據(jù)結(jié)構(gòu) sighand_struct。這里最主要的是維護信號處理函數(shù)莱衩,信號處理函數(shù)會從父進程復(fù)制到子進程爵嗅。
  3. 進程內(nèi)存空間相關(guān)的數(shù)據(jù)結(jié)構(gòu) mm_struct。
  4. copy_process 后續(xù)還會分配 pid膳殷,設(shè)置 tid操骡,group_leader,并且建立進程之間的親緣關(guān)系赚窃。

進程初始化的內(nèi)容不止上述內(nèi)容册招,僅僅做簡單記錄,corp_process函數(shù)執(zhí)行完進程初始化結(jié)束后勒极,就需要喚醒進程了是掰。

  1. 設(shè)置該進程的狀態(tài)TASK_RUNNING
  2. 根據(jù)不同的調(diào)度類來進入到不同調(diào)度類對應(yīng)的進程隊列中。更新隊列的進程數(shù)
  3. 檢查該進程能否搶占當(dāng)前本進程辱匿。即父進程键痛,從上述內(nèi)容知道創(chuàng)建子進程是通過fork函數(shù)來的,這是一個系統(tǒng)調(diào)用匾七,所以從內(nèi)核態(tài)返回到用戶態(tài)的時候父進程會判斷被強占變量是否需要搶占絮短,來執(zhí)行__schedule函數(shù)讓出給子進程運行
undefined

線程

對于任何一個進程來講昨忆,即便沒有主動去創(chuàng)建線程丁频,進程也是默認有一個主線程的。線程可以將項目并行起來邑贴,加快進度席里,但是也帶來的負面影響,數(shù)據(jù)的處理拢驾。

undefined

線程棧的大小可以通過命令 ulimit -a 查看奖磁,默認情況下線程棧大小為 8192(8MB)》卑蹋可以使用命令 ulimit -s 修改咖为。主線程在內(nèi)存中有一個棧空間稠腊,其他線程棧也擁有獨立的棸钙#空間。為了避免線程之間的椔檠空間踩踏,線程棧之間還會有小塊區(qū)域诺舔,用來隔離保護各自的棻畈空間备畦。一旦另一個線程踏入到這個隔離區(qū),就會引發(fā)段錯誤许昨。

線程間協(xié)作互斥有兩種方式懂盐,一種是搶占鎖和被動等待。一種是搶占鎖通知糕档。通知的方式明顯好于被動等待的運行模式莉恼,會減少CPU的開銷且減少線程的無效等待時間。這種方式也是條件變量和互斥鎖是配合使用的

線程的創(chuàng)建

每一個進程或者線程都有一個 task_struct 結(jié)構(gòu)速那,在用戶態(tài)也有一個用于維護線程的結(jié)構(gòu)俐银,就是 pthread 結(jié)構(gòu)。凡是涉及函數(shù)的調(diào)用端仰,都要使用到棧。每個線程也有自己的棧。所以需要先為線程來創(chuàng)建線程棧怠苔。搞定了用戶態(tài)棧的問題刚盈,其實用戶態(tài)的事情基本搞定了一半。

內(nèi)容中真正創(chuàng)建線程的是調(diào)用 create_thread 函數(shù)鹤竭。該函數(shù)除了針對線程棧執(zhí)行了一些特定的操作后踊餐,最后調(diào)用的仍是_do_fork函數(shù)和進程創(chuàng)建時調(diào)用的一樣。需要注意的就是do_fork函數(shù)中很多變量和數(shù)據(jù)結(jié)構(gòu)的初始化有區(qū)別于進程初始化過程臀稚,五大結(jié)構(gòu)僅僅是引用計數(shù)加一吝岭,也即線程共享進程的數(shù)據(jù)結(jié)構(gòu)

在用戶的函數(shù)執(zhí)行完畢之后烁涌,會釋放這個線程相關(guān)的數(shù)據(jù)苍碟。例如,線程本地數(shù)據(jù) thread_local variables撮执,線程數(shù)目也減一微峰。如果這是最后一個線程了,就直接退出進程抒钱, 線程的線程棧要從當(dāng)前使用線程棧的列表 stack_used 中拿下來蜓肆,放到緩存的線程棧列表 stack_cache 中。

undefined

調(diào)度

Linux 里面谋币,進程分成兩種仗扬,通過task_struct 中的兩個成員變量來區(qū)分,調(diào)度策略蕾额,優(yōu)先級早芭。

  • 實時進程:需要盡快執(zhí)行返回結(jié)果。優(yōu)先級較高
  • 普通進程:大部分的進程其實都是這種诅蝶。

實時調(diào)度策略:

  • SCHED_FIFO:高優(yōu)先級的進程可以搶占低優(yōu)先級的進程退个,而相同優(yōu)先級的進程募壕,遵循先來先得
  • SCHED_RR(輪循):采用時間片,相同優(yōu)先級的任務(wù)當(dāng)用完時間片會被放到隊列尾部语盈,以保證公平性舱馅,而高優(yōu)先級的任務(wù)也是可以搶占低優(yōu)先級的任務(wù)。
  • SCHED_DEADLINE:按照任務(wù)的deadline進行調(diào)度刀荒。當(dāng)產(chǎn)生一個調(diào)度點的時候代嗤,DL 調(diào)度器總是選擇其 deadline距離當(dāng)前時間點最近的那個任務(wù),并調(diào)度它執(zhí)行缠借。

普通調(diào)度策略:

  • SCHED_NORMAL:普通的進程
  • SCHED_BATCH:后臺進程干毅,幾乎不需要和前端進行交互。這類項目可以默默執(zhí)行烈炭,不要影響需要交互的進程溶锭,可以降低它的優(yōu)先級。
  • SCHED_IDLE:特別空閑的時候才跑的進程

對于CPU執(zhí)行的大部分任務(wù)都是普通進程符隙,普通進程使用的調(diào)度策略是fair_sched_class趴捅,公平調(diào)度策略。針對此策略的算法實現(xiàn)即CFS 的調(diào)度算法霹疫,即記錄下各進程運行的時間拱绑。CPU會提供一個時鐘,過一段時間就觸發(fā)一個時鐘中斷丽蝎,即tick猎拨。CFS會為每一個進程安排一個虛擬運行時間vruntime。如果一個進程在運行屠阻,隨著時間的增長红省,也就是一個個 tick 的到來,進程的 vruntime將不斷增大国觉。++沒有得到執(zhí)行的進程vruntime不變吧恃。所以會優(yōu)先運行vruntime小的進++程。

需要考慮不同優(yōu)先級的進程麻诀,虛擬運行時間的計算規(guī)則如下:

虛擬運行時間 vruntime += 實際運行時間 delta_exec * NICE_0_LOAD/ 權(quán)重

各調(diào)度策略都有自己的一個數(shù)據(jù)結(jié)構(gòu)用來進行排序痕寓。各進程根據(jù)自己是實時的,還是普通的類型蝇闭,通過這個成員變量呻率,將自己掛在某一個數(shù)據(jù)結(jié)構(gòu)里面,和其他的進程排序呻引,等待被調(diào)度礼仗。 所有可運行的進程通過不斷地插入操作最終都存儲在以時間為順序的紅黑樹中,vruntime 最小的在樹的左側(cè),vruntime 最多的在樹的右側(cè)藐守。 CFS 調(diào)度策略會選擇紅黑樹最左邊的葉子節(jié)點作為下一個將獲得 CPU 的任務(wù)挪丢。

在每個 CPU 上都有一個隊列 rq,這個隊列里面包含多個子隊列卢厂,不同的隊列有不同的實現(xiàn)方式,cfs_rq 就是用紅黑樹實現(xiàn)的惠啄。當(dāng)有一天慎恒,某個CPU需要找下一個任務(wù)執(zhí)行的時候,會按照優(yōu)先級依次調(diào)用調(diào)度類撵渡,不同的調(diào)度類操作不同的隊列融柬。當(dāng)然 rt_sched_class 先被調(diào)用,它會在rt_rq上找下一個任務(wù)趋距,只有找不到的時候粒氧,才輪到 fair_sched_class 被調(diào)用,它會在 cfs_rq 上找下一個任務(wù)节腐。這樣保證了實時任務(wù)的優(yōu)先級永遠大于普通任務(wù)外盯。

undefined

搶占式調(diào)度

進程間常常會發(fā)生搶占式調(diào)度。一個進程執(zhí)行時間太長了翼雀,就是時候切換到另一個進程了饱苟。計算機中有一個時鐘,會過一段時間觸發(fā)一次時鐘中斷狼渊,通知操作系統(tǒng)箱熬,時間又過去一個時鐘周期,這是個很好的方式狈邑,可以查看是否是需要搶占的時間點城须。如上述當(dāng)內(nèi)容中,當(dāng)一次時鐘中斷過來后米苹,觸發(fā)更新進程的vruntime糕伐,后續(xù)會調(diào)用check_preempt_tick。顧名思義就是驱入,檢查是否是時候被搶占了赤炒。當(dāng)發(fā)現(xiàn)當(dāng)前進程應(yīng)該被搶占,不能直接把它踢下來亏较,而是把它標(biāo)記為應(yīng)該被搶占莺褒。 一定要等待正在運行的進程調(diào)用 __schedule 才行,所以這里只能先標(biāo)記一下雪情。

另外一個可能搶占的場景是當(dāng)一個進程被喚醒的時候遵岩。當(dāng)被喚醒的進程優(yōu)先級高于當(dāng)前運行的進程的時候就會發(fā)生搶占。這里的搶占也并不是立即踢掉當(dāng)前進程,而是將當(dāng)前進程標(biāo)記為可搶占尘执。

搶占時機

用戶態(tài)的搶占時機

真正的搶占還需要時機舍哄,也就是需要那么一個時刻,讓正在運行中的進程有機會調(diào)用一下__schedule誊锭。對于用戶態(tài)的進程來講表悬,從系統(tǒng)調(diào)用中返回的那個時刻,是一個被搶占的時機從內(nèi)核態(tài)返回到用戶態(tài)的時候丧靡,內(nèi)核態(tài)中的函數(shù)會對當(dāng)前進程的標(biāo)記為進行檢查蟆沫,如果需要被搶占即會執(zhí)行__schedule函數(shù)。對于用戶態(tài)的進程來講温治,從中斷中返回的那個時刻饭庞,也是一個被搶占的時機。 和從內(nèi)核態(tài)返回到用戶態(tài)函數(shù)代碼的檢查邏輯一樣熬荆,中斷處理函數(shù)執(zhí)行后也會對該標(biāo)志位進行判斷舟山。

內(nèi)核態(tài)的搶占時機

在內(nèi)核態(tài)的執(zhí)行中,有的操作是不能被中斷的卤恳,所以在進行這些操作之前累盗,總是先調(diào)用 preempt_disable() 關(guān)閉搶占,當(dāng)再次打開的時候(內(nèi)核態(tài)代碼打開邏輯寫在了特定函數(shù)執(zhí)行步驟時)纬黎,就是一次內(nèi)核態(tài)代碼被搶占的機會幅骄。在內(nèi)核態(tài)也會遇到中斷的情況,當(dāng)中斷返回的時候本今,返回的仍然是內(nèi)核態(tài)拆座。這個時候也是一個執(zhí)行搶占的時機,和用戶態(tài)中斷返回觸發(fā)搶占邏輯一致冠息。

undefined

內(nèi)存管理

進程無法直接操作物理內(nèi)存的原因:不同進程并行運行的時候挪凑,同一地址的物理內(nèi)存被多個進程同時修改,會出現(xiàn)丟失修改的情況逛艰。

出于對上述風(fēng)險的規(guī)避躏碳,操作系統(tǒng)并不會給每個進程內(nèi)存地址的實際物理地址。內(nèi)存實際的物理地址對于進程來說是不可見的散怖,但操作系統(tǒng)會給每個進程分配一個虛擬地址菇绵。所有進程看到的這個地址都是一樣的,里面的內(nèi)存都是從 0 開始編號镇眷。

在程序里面咬最,指令寫入的地址是虛擬地址。操作系統(tǒng)會提供一種機制欠动,將不同進程的虛擬地址和不同內(nèi)存的物理地址映射起來永乌。 當(dāng)程序要訪問虛擬地址的時候惑申,由內(nèi)核的數(shù)據(jù)結(jié)構(gòu)進行轉(zhuǎn)換,轉(zhuǎn)換成不同的物理地址翅雏,這樣不同的進程運行的時候圈驼,寫入的是不同的物理地址,這樣就不會沖突了

簡單的程序在使用內(nèi)存時的幾種方式:用戶態(tài)階段

  • 代碼需要放在內(nèi)存里面望几;
  • 全局變量绩脆,例如 max_length;
  • 常量字符串"Input the string length : "橄妆;
  • 函數(shù)棧衙伶,例如局部變量 num 是作為參數(shù)傳給 generate 函數(shù)的,這里面涉及了函數(shù)調(diào)用害碾,局部變量,函數(shù)參數(shù)等都是保存在函數(shù)棧上面的赦拘;
  • 堆慌随,malloc 分配的內(nèi)存在堆里面;
  • 這里面涉及對 glibc 的調(diào)用躺同,所以 glibc 的代碼是以 so 文件的形式存在的阁猜,也需要放在內(nèi)存里面。

內(nèi)核部分還需要分配內(nèi)存:

  • 內(nèi)核中也有全局變量蹋艺;
  • 每個進程都要有一個 task_struct剃袍;
  • 每個進程還有一個內(nèi)核棧;
  • 在內(nèi)核里面也有動態(tài)分配的內(nèi)存捎谨;
  • 虛擬地址到物理地址的映射表

用戶態(tài)和內(nèi)核態(tài)都是通過虛擬地址來訪問物理內(nèi)存的民效。如果是 32 位的機器,有 2^32 = 4G 的內(nèi)存空間都是進程可以操作的內(nèi)存空間涛救,雖然是虛擬的畏邢。 這部分內(nèi)存空間,用戶態(tài)內(nèi)存在內(nèi)存地址的低地址检吆,內(nèi)核態(tài)內(nèi)存在內(nèi)存地址高地址舒萎。而且對于普通進程無權(quán)限訪問內(nèi)核內(nèi)存地址。所以蹭沛,用戶態(tài)只能通過系統(tǒng)調(diào)用進入內(nèi)核態(tài)臂寝,進入內(nèi)核態(tài)之后各進程之間就成了互相可見的狀態(tài),雖然內(nèi)核棧是各用各的摊灭,但其他的數(shù)據(jù)結(jié)構(gòu)是共同的同一批數(shù)據(jù)結(jié)構(gòu)咆贬,所以需要進行鎖保護。并且內(nèi)核態(tài)也無法訪問內(nèi)核態(tài)的內(nèi)存空間斟或。

虛擬地址

分段機制下的虛擬地址由兩部分組成素征,段選擇子和段內(nèi)偏移量。段選擇子里面最重要的是段號,用作段表的索引御毅,段表里面保存的是這個段的基地址根欧、段的界限和特權(quán)等級等。段基地址加上段內(nèi)偏移量得到物理內(nèi)存地址端蛆。但在 Linux 操作系統(tǒng)中凤粗,并沒有使用到全部的分段功能。只有部分權(quán)限審核的時候才會用到今豆。

Linux 傾向于另外一種從虛擬地址到物理地址的轉(zhuǎn)換方式嫌拣,稱為分頁。對于物理內(nèi)存呆躲,操作系統(tǒng)把它分成一塊一塊大小相同的頁异逐,這樣更方便管理,例如有的內(nèi)存頁面長時間不用了插掂,可以暫時寫到硬盤上灰瞻,稱為換出。一旦需要的時候辅甥,再加載進來酝润,叫做換入。這樣可以擴大可用物理內(nèi)存的大小璃弄,提高物理內(nèi)存的利用率要销。

換入和換出都是以頁為單位的。頁面的大小一般為 4KB夏块,為了能夠定位和訪問每個頁疏咐,需要有個頁表,保存每個頁的起始地址拨扶,再加上在頁內(nèi)的偏移量凳鬓,組成線性地址,就能對于內(nèi)存中的每個位置進行訪問了患民。

undefined

虛擬地址分為兩部分缩举,頁號頁內(nèi)偏移。頁號作為頁表某一具體虛擬頁號的索引匹颤,頁表包含物理頁每頁所在物理內(nèi)存的基地址仅孩。這個基地址與頁內(nèi)偏移的組合就形成了物理內(nèi)存地址。物理內(nèi)存地址 = 物理內(nèi)存的基地址 + 頁內(nèi)偏移印蓖。

虛擬內(nèi)存中的頁通過頁表映射為了物理內(nèi)存中的頁:32 位環(huán)境下辽慕,虛擬地址空間共 4GB。如果分成 4KB 一個頁赦肃,那就是 1M 個頁溅蛉。頁表的每個頁表項需要 4 個字節(jié)來存儲公浪,那么整個 4GB 虛擬內(nèi)存空間的映射就需要 4MB 的內(nèi)存來存儲映射表。如果每個進程都有自己的映射表船侧,100 個進程就需要 400MB 的內(nèi)存欠气。對于內(nèi)核來講,有點大了 镜撩。頁表中所有頁表項必須提前建好预柒,并且要求是連續(xù)的。如果不連續(xù)袁梗,就沒有辦法通過虛擬地址里面的頁號找到對應(yīng)的頁表項了宜鸯。

后續(xù)改進:將頁表再分頁,4GB 的空間需要 4MB 的頁表來存儲映射遮怜。我們把這 4M 分成 1K(1024)個 4KB淋袖,每個 4KB 又能放在一頁里面,這樣 1K 個 4KB 就是 1K 個頁锯梁,這 1K 個頁也需要一個表進行管理适贸,我們稱為頁目錄表,這個頁目錄表里面有 1K 項涝桅,每項 4 個字節(jié),頁目錄表大小也是 4K烙样。

頁目錄有 1K 項冯遂,用 10 位就可以表示訪問頁目錄的哪一項。這一項其實對應(yīng)的是一整頁的頁表項谒获,也即 4K 的頁表項蛤肌。每個頁表項也是 4 個字節(jié),因而一整頁的頁表項定位其實需要 1K 個地址批狱。再用 10 位就可以表示訪問頁表項的哪一項裸准。

頁表項中的一項對應(yīng)的就是一個頁,是存放數(shù)據(jù)的頁赔硫,這個頁的大小是4K炒俱,用12位可以定位這個頁內(nèi)的任何一個位置。

這樣加起來正好 32 位爪膊,也就是用前 10 位定位到頁目錄表中的一項权悟。將這一項對應(yīng)的頁表取出來共 1k項,再用中間 10 位定位到頁表中的一項推盛,將這一項對應(yīng)的存放數(shù)據(jù)的頁取出來峦阁,再用最后12位定位到頁中的具體位置訪問數(shù)據(jù)(頁內(nèi)偏移)。

undefined

這樣雖然看上去一個為訪問一個進程的內(nèi)存空間耘成。所需要的消耗的虛擬地址空間更大了榔昔,但其實我們并不會給每個進程分配這么大的內(nèi)存空間驹闰。所以其實頁表項雖然可以有1M個,即我們?yōu)檫M程要分配1M個數(shù)據(jù)頁的話撒会,每個4byte總共4MB嘹朗。頁目錄項最多1k個,每個4byte總共4kb茧彤。即4Mb+4kb骡显。<< 400Mb

但綜上,如果只給進程分配了一個數(shù)據(jù)頁的話曾掂,即4kb的內(nèi)存惫谤,那我們理論上也只需要4byte的頁表項即1個頁表項就能定位到該內(nèi)存地址。但由于頁目錄項珠洗,1個頁目錄項單位對應(yīng)頁表項1kb個溜歪。所以我們需要的最少其實是1個頁目錄項,也即许蓖,1個頁目錄項->4kb頁表項蝴猪。即4kb的空間來表示內(nèi)存空間即可。

當(dāng)然對于 64 位的系統(tǒng)膊爪,兩級肯定不夠了自阱,就變成了四級目錄,分別是全局頁目錄項PGD米酬、上層頁目錄項PUD沛豌、中間頁目錄項 PMD和頁表項 PTE。

undefined

內(nèi)存管理系統(tǒng)精細化為下面三件事情:

  1. 虛擬內(nèi)存空間的管理赃额,將虛擬內(nèi)存分成大小相等的頁加派;
  2. 物理內(nèi)存的管理弛房,將物理內(nèi)存分成大小相等的頁汰寓;
  3. 內(nèi)存映射,將虛擬內(nèi)存頁和物理內(nèi)存頁映射起來裹虫,并且在內(nèi)存緊張的時候可以換出到硬盤中飞盆。
undefined

整個進程的虛擬內(nèi)存空間要一分為二娄琉,一部分是用戶態(tài)地址空間,一部分是內(nèi)核態(tài)地址空間桨啃。對于 32位系統(tǒng)车胡,最大能夠?qū)ぶ?^32=4G,其中用戶態(tài)虛擬地址空間是 3G照瘾,內(nèi)核態(tài)是 1G匈棘。對于 64 位系統(tǒng),虛擬地址只使用了 48 位析命。其中主卫,用戶態(tài)空間和內(nèi)核空間都是 128T逃默。內(nèi)核空間和用戶空間之間隔著很大的空隙,以此來進行隔離簇搅。

undefined

我們知道完域,這么大的虛擬地址空間,不可能都有真實內(nèi)存對應(yīng)瘩将,所以這里是映射的數(shù)目吟税。當(dāng)內(nèi)存吃緊的時候,有些頁可以換出到硬盤上姿现,有的頁因為比較重要肠仪,不能換出。虛擬內(nèi)存區(qū)域可以映射到物理內(nèi)存备典,也可以映射到文件异旧,映射到物理內(nèi)存的時候稱為匿名映射。

進程的用戶態(tài)內(nèi)存空間中各數(shù)據(jù)結(jié)構(gòu)的結(jié)構(gòu)圖及操作系統(tǒng)對應(yīng)函數(shù):


undefined

內(nèi)核態(tài)的虛擬空間和某一個進程沒有關(guān)系提佣,所有進程通過系統(tǒng)調(diào)用進入到內(nèi)核之后吮蛹,看到的虛擬地址空間都是一樣的。內(nèi)核態(tài)所以進程公用虛擬地址空間拌屏。

進程狀態(tài)在64位操作系統(tǒng)下關(guān)系:

undefined

CPU訪問內(nèi)存的兩種模式:

  1. SMP(對稱多處理器模式):CPU 會有多個潮针,在總線的一側(cè)。所有的內(nèi)存條組成一大片內(nèi)存倚喂,在總線的另一側(cè)然低,所有的 CPU 訪問內(nèi)存都要過總線,而且距離都是一樣的务唐。缺點是,總線會成為瓶頸带兜,因為所有的數(shù)據(jù)訪問都走它枫笛。
  2. NUMA(非一致內(nèi)存訪問):在這種模式下,內(nèi)存不是一整塊刚照。每個CPU都有自己的本地內(nèi)存刑巧,CPU訪問本地內(nèi)存不用過總線,因而速度要快很多无畔,每個CPU和內(nèi)存在一起啊楚,稱為一個NUMA節(jié)點。但是浑彰,在本地內(nèi)存不足的情況下恭理,每個CPU都可以去另外的NUMA節(jié)點申請內(nèi)存,這個時候訪問延時就會比較長郭变。NUMA往往是非連續(xù)內(nèi)存模型颜价。以上也就是L1,L2,L3CPU三季緩存的場景涯保。

頁面換出

每個進程都有自己的虛擬地址空間,無論是32位還是64位周伦,虛擬地址空間都非常大夕春,物理內(nèi)存不可能有這么多的空間放得下。所以专挪,一般情況下及志,頁面只有在被使用的時候,才會放在物理內(nèi)存中寨腔。++如果過了一段時間不被使用++速侈,即便用戶進程并沒有釋放它,物理內(nèi)存管理會將這些物理內(nèi)存中的頁面換出到硬盤上去脆侮;將空出的物理內(nèi)存锌畸,交給活躍的進程去使用

頁面換出的觸發(fā)時機

  1. 分配內(nèi)存的時候,發(fā)現(xiàn)沒有地方了靖避,就試圖回收一下潭枣。
  2. 內(nèi)核線程 kswapd,當(dāng)內(nèi)存緊張時幻捏,會檢查一下內(nèi)存盆犁,看看是否需要換出一些內(nèi)存頁。不緊張時篡九,無限循環(huán)谐岁,并不做實際處理¢痪剩基本原理是通過LRU算法來找出最不活躍的內(nèi)存頁來進行換出伊佃。

文件系統(tǒng)

當(dāng)使用系統(tǒng)調(diào)用 open 打開一個文件時,操作系統(tǒng)會創(chuàng)建一些數(shù)據(jù)結(jié)構(gòu)來表示這個被打開的文件沛善。在進程中航揉,我們會為這個打開的文件分配一個文件描述符 fd,文件描述符金刁,就是用來區(qū)分一個進程打開的多個文件的帅涂。它的作用域就是當(dāng)前進程,出了當(dāng)前進程這個文件描述符就沒有意義了尤蛮。我們對這個文件的所有操作都要靠這個 fd媳友,包括最后關(guān)閉文件。

磁盤上下多層分多個盤片产捞,每一層里分多個磁道醇锚,每個磁道分多個扇區(qū),每個扇區(qū)是 512 個字節(jié)坯临。硬盤會分成相同大小的單元搂抒,稱為塊艇搀。一塊的大小是扇區(qū)大小的整數(shù)倍,默認是 4K求晶。

一種特殊的文件格式焰雕,硬鏈接和軟鏈接。ln -s 創(chuàng)建的是軟鏈接芳杏,不帶 -s 創(chuàng)建的是硬鏈接

  • 硬鏈接與原始文件共用一個 inode 的矩屁,但是 inode 是不跨文件系統(tǒng)的,每個文件系統(tǒng)都有自己的 inode 列表爵赵,因而硬鏈接是沒有辦法跨文件系統(tǒng)的吝秕。
  • 軟鏈接不同,軟鏈接相當(dāng)于重新創(chuàng)建了一個文件空幻。這個文件也有獨立的 inode烁峭,只不過打開這個文件看里面內(nèi)容的時候,內(nèi)容指向另外的一個文件秕铛。這就很靈活了约郁。我們可以跨文件系統(tǒng),甚至目標(biāo)文件被刪除了但两,鏈接文件還是在的鬓梅,只不過指向的文件找不到了而已。

緩存其實就是內(nèi)存中的一塊空間谨湘。因為內(nèi)存比硬盤快得多绽快,Linux為了改進性能,有時候會選擇不直接操作硬盤紧阔,而是讀寫都在內(nèi)存中坊罢,然后批量讀取或者寫入硬盤。一旦能夠命中內(nèi)存擅耽,讀寫效率就會大幅度提高艘绍。

根據(jù)是否使用內(nèi)存做緩存,可以把文件的 I/O 操作分為兩種類型秫筏。

  • 緩存 I/O:大多數(shù)文件系統(tǒng)的默認 I/O 操作都是緩存 I/O。對于讀操作來講挎挖,操作系統(tǒng)會先檢查这敬,內(nèi)核的緩沖區(qū)有沒有需要的數(shù)據(jù)。如果已經(jīng)緩存了蕉朵,那就直接從緩存中返回崔涂;否則從磁盤中讀取,然后緩存在操作系統(tǒng)的緩存中始衅。對于寫操作來講冷蚂,操作系統(tǒng)會先將數(shù)據(jù)從用戶空間復(fù)制到內(nèi)核空間的緩存中缭保。這時對用戶程序來說,寫操作就已經(jīng)完成蝙茶。至于什么時候再寫到磁盤中由操作系統(tǒng)決定艺骂,除非顯式地調(diào)用了 sync 同步命令。 文件編輯中Ctrl S
  • 直接 IO:應(yīng)用程序直接訪問磁盤數(shù)據(jù)隆夯,而不經(jīng)過內(nèi)核緩沖區(qū)钳恕,從而減少了在內(nèi)核緩存和用戶程序之間數(shù)據(jù)復(fù)制。

輸入輸出設(shè)備

CPU 并不直接和設(shè)備打交道蹄衷,它們中間有一個叫作設(shè)備控制器的組件忧额,例如硬盤有磁盤控制器、USB有USB控制器愧口、顯示器有視頻控制器等睦番。這些控制器就像不同地區(qū)的代理商一樣,它們知道如何應(yīng)對硬盤耍属、鼠標(biāo)托嚣、鍵盤、顯示器等不同類型的外接設(shè)備的行為恬涧。

這些控制器有它自己的寄存器注益。這樣CPU就可以通過寫這些寄存器,對控制器下發(fā)指令溯捆,通過讀這些寄存器丑搔,查看控制器對于設(shè)備的操作狀態(tài)。CPU 對于寄存器的讀寫提揍,要比直接控制硬件啤月,要標(biāo)準(zhǔn)和輕松很多

輸入輸出設(shè)備大致可以分為兩類:塊設(shè)備和字符設(shè)備劳跃。

  • 塊設(shè)備將信息存儲在固定大小的塊中谎仲,每個塊都有自己的地址。硬盤就是常見的塊設(shè)備刨仑。
  • 字符設(shè)備發(fā)送或接收的是字節(jié)流郑诺。而不用考慮任何塊結(jié)構(gòu),沒有辦法尋址杉武。鼠標(biāo)就是常見的字符設(shè)備

由于塊設(shè)備傳輸?shù)臄?shù)據(jù)量比較大辙诞,++控制器里往往會有緩沖區(qū)。CPU寫入緩沖區(qū)的數(shù)據(jù)攢夠一部分轻抱,才會發(fā)給設(shè)備++++飞涂。CPU讀取的數(shù)據(jù),也需要在緩沖區(qū)攢夠一部分,才拷貝到內(nèi)存++

CPU同控制器的寄存器和數(shù)據(jù)緩沖區(qū)進行通信的方式

  • 每個控制寄存器被分配一個 I/O 端口较店,通過特殊的匯編指令操作這些寄存器士八。
  • 數(shù)據(jù)緩沖區(qū),可內(nèi)存映射I/O梁呈,可以分配一段內(nèi)存空間給它婚度,就像讀寫內(nèi)存一樣讀寫數(shù)據(jù)緩沖區(qū)

CPU和設(shè)備讀取數(shù)據(jù)交互的方式捧杉,控制器的寄存器一般會有狀態(tài)標(biāo)志位陕见,可以通過檢測狀態(tài)標(biāo)志位,來確定輸入或者輸出操作是否完成味抖。

  1. 輪詢等待:就是一直查评甜,一直查,直到完成仔涩。當(dāng)然這種方式很不好忍坷。
  2. 中斷的方式:通知操作系統(tǒng)輸入輸出操作已經(jīng)完成。

為了響應(yīng)中斷熔脂,我們一般會有一個硬件的中斷控制器佩研,當(dāng)設(shè)備完成任務(wù)后觸發(fā)中斷到中斷控制器,中斷控制器就通知 CPU霞揉,一個中斷產(chǎn)生了旬薯,CPU 需要停下當(dāng)前手里的事情來處理中斷。

有的設(shè)備需要讀取或者寫入大量數(shù)據(jù)适秩。如果所有過程都讓CPU協(xié)調(diào)的話绊序,就需要占用CPU大量的時間,比方說秽荞,磁盤就是這樣的骤公。這種類型的設(shè)備需要支持 DMA 功能,也就是說扬跋,允許設(shè)備在CPU不參與的情況下阶捆,能夠自行完成對內(nèi)存的讀寫。實現(xiàn) DMA 機制需要有個 DMA 控制器幫你的 CPU 來做協(xié)調(diào)钦听。

CPU 只需要對 DMA控制器下指令洒试,說它想讀取多少數(shù)據(jù),放在內(nèi)存的某個地方就可以了朴上,接下來DMA控制器會發(fā)指令給磁盤控制器垒棋,讀取磁盤上的數(shù)據(jù)到指定的內(nèi)存位置,傳輸完畢之后余指,DMA 控制器發(fā)中斷通知 CPU 指令完成,CPU 就可以直接用內(nèi)存里面現(xiàn)成的數(shù)據(jù)了。

這里需要注意的是酵镜,設(shè)備控制器不屬于操作系統(tǒng)的一部分碉碉,但是設(shè)備驅(qū)動程序?qū)儆诓僮飨到y(tǒng)的一部分。操作系統(tǒng)的內(nèi)核代碼可以像調(diào)用本地代碼一樣調(diào)用驅(qū)動程序的代碼淮韭,而驅(qū)動程序的代碼需要發(fā)出特殊的面向設(shè)備控制器的指令垢粮,才能操作設(shè)備控制器。一個設(shè)備驅(qū)動程序初始化的時候靠粪,要先注冊一個該設(shè)備的中斷處理函數(shù)

undefined

進程間通信

  1. 管道模型(命令行中常用):前一個命令的輸出蜡吧,作為后一個命令的輸入。管道是一種單向傳輸數(shù)據(jù)的機制占键,它其實是一段緩存昔善,里面的數(shù)據(jù)只能從一端寫入,從另一端讀出畔乙。
ps -ef | grep 關(guān)鍵字 | awk '{print $2}' | xargs kill -9

“|” 表示的管道稱為匿名管道君仆。豎線代表的管道隨著命令的執(zhí)行自動創(chuàng)建、自動銷毀牲距。

  1. 消息隊列模型(不常用):發(fā)送數(shù)據(jù)時返咱,會分成一個一個獨立的數(shù)據(jù)單元,也就是消息體牍鞠,每個消息體都是固定大小的存儲塊咖摹,在字節(jié)流上不連續(xù)。
  2. 共享內(nèi)存模型(常用):彌補消息隊列需要數(shù)據(jù)傳遞难述,消息傳送還是不及時萤晴。如果一個進程想要訪問這一段共享內(nèi)存,需要將這個內(nèi)存加載到自己的虛擬地址空間的某個位置龄广。 需要和信號量一起使用硫眯。
  3. 信號量(常用):防止共享內(nèi)存同一時間,兩進程寫同一內(nèi)存空間產(chǎn)生沖突的情況择同。所以两入,信號量和共享內(nèi)存往往要配合使用。信號量其實是一個計數(shù)器敲才,主要用于實現(xiàn)進程間的互斥與同步裹纳,而不是用于存儲進程間通信數(shù)據(jù)。
  4. 信號(常用):信號可以在任何時候發(fā)送給某一進程紧武,進程需要為這個信號配置信號處理函數(shù)剃氧。當(dāng)某個信號發(fā)生的時候,就默認執(zhí)行這個函數(shù)就可以了阻星。

Linux 操作系統(tǒng)中朋鞍,為了響應(yīng)各種各樣的事件已添,也是定義了非常多的信號。

undefined

管道:無論是匿名管道滥酥,還是命名管道更舞,在內(nèi)核都是一個文件。只要是文件就要有一個 inode坎吻。管道的inode++對應(yīng)內(nèi)存里面的緩存++缆蝉。寫入一個 pipe 就是從 struct file 結(jié)構(gòu)找到緩存寫入,讀取一個 pipe 就是從 struct file 結(jié)構(gòu)找到緩存讀出瘦真。

網(wǎng)絡(luò)

一臺機器將自己想要表達的內(nèi)容刊头,按照某種約定好的格式發(fā)送出去,當(dāng)另外一臺機器收到這些信息后诸尽,也能夠按照約定好的格式解析出來原杂,從而準(zhǔn)確、可靠地獲得發(fā)送方想要表達的內(nèi)容弦讽。這種約定好的格式就是網(wǎng)絡(luò)協(xié)議污尉。

網(wǎng)絡(luò)協(xié)議模型有兩種,一種是OSI 的標(biāo)準(zhǔn)七層模型往产,一種是業(yè)界標(biāo)準(zhǔn)的 TCP/IP 模型被碗。其中也有五層模型,其實就是刪除了表示層和會話層仿村。網(wǎng)絡(luò)分層的原因是锐朴,網(wǎng)絡(luò)環(huán)境過于復(fù)雜,不是一個能夠集中控制的體系蔼囊。

undefined

網(wǎng)絡(luò)層

ip地址:192.168.1.100/24焚志,斜杠前面是 IP 地址,這個地址被.分隔為四個部分畏鼓,每個部分 8 位酱酬,總共是 32 位。斜線后面 24 的意思是云矫,32位中膳沽,前24位是網(wǎng)絡(luò)號,后8位是主機號让禀。IP地址類似互聯(lián)網(wǎng)上的郵寄地址挑社,是有全局定位功能的。

路由協(xié)議:將網(wǎng)絡(luò)包從一個網(wǎng)絡(luò)轉(zhuǎn)發(fā)給另一個網(wǎng)絡(luò)的設(shè)備稱為路由器巡揍。網(wǎng)絡(luò)包從一個起始的 IP 地址痛阻,沿著路由協(xié)議指的道兒,經(jīng)過多個網(wǎng)絡(luò)腮敌,通過多次路由器轉(zhuǎn)發(fā)阱当,到達目標(biāo) IP 地址俏扩。

數(shù)據(jù)鏈路層

第二層。也叫MAC層弊添,MAC主要做的就是網(wǎng)絡(luò)包在本地網(wǎng)絡(luò)中的服務(wù)器之間定位及通信的機制动猬。所謂MAC,就是每個網(wǎng)卡都有的唯一的硬件地址(不絕對唯一表箭,相對大概率唯一即可,類比UUID)钮莲。這雖然也是一個地址免钻,但是這個地址是沒有在整個互聯(lián)網(wǎng)中全局定位功能的,只能在本地網(wǎng)絡(luò)中通過ARP協(xié)議來定位崔拥。

MAC 地址的定位功能局限在一個網(wǎng)絡(luò)里面极舔,也即同一個網(wǎng)絡(luò)號下的 IP 地址之間,可以通過 MAC 進行定位和通信链瓦。從 IP 地址獲取 MAC 地址要通過 ARP 協(xié)議拆魏,是通過在本地發(fā)送廣播包,獲得的 MAC 地址慈俯。MAC 地址的作用范圍不能出本地網(wǎng)絡(luò)渤刃,所以一旦跨網(wǎng)絡(luò)通信,雖然 IP 地址保持不變,但是 MAC 地址每經(jīng)過一個路由器就要換一次。

undefined

如圖服務(wù)器 A 發(fā)送網(wǎng)絡(luò)包給服務(wù)器B喻喳,原IP地址始終是192.168.1.100舔箭,目標(biāo)IP地址始終是192.168.2.100,但是在網(wǎng)絡(luò) 1 里面颊郎,原 MAC 地址是 MAC1,目標(biāo)MAC地址是路由器的MAC2。路由器內(nèi)部也從MAC2轉(zhuǎn)發(fā)到了MAC3诫舅,路由器轉(zhuǎn)發(fā)之后,原 MAC 地址是路由器的 MAC3宫患,目標(biāo) MAC 地址是 MAC4刊懈。

傳輸層

第四層,傳輸層撮奏,這里有兩個著名的協(xié)議TCP 和 UDP俏讹。在 IP 層的代碼邏輯中,僅僅負責(zé)數(shù)據(jù)從一個 IP 地址發(fā)送給另一個 IP 地址畜吊,丟包泽疆、亂序、重傳玲献、擁塞殉疼,這些 IP 層都不管梯浪。處理這些問題的代碼邏輯寫在了傳輸層的 TCP 協(xié)議里面。 從第一層到第三層都不可靠瓢娜,網(wǎng)絡(luò)包說丟就丟挂洛,是 TCP 這一層通過各種編號、重傳等機制眠砾,讓本來不可靠的網(wǎng)絡(luò)對于更上層來講虏劲,變得“看起來”可靠。

二層到四層都是在 Linux 內(nèi)核里面處理的褒颈,應(yīng)用層例如瀏覽器柒巫、Nginx、Tomcat 都是用戶態(tài)的谷丸。內(nèi)核里面對于網(wǎng)絡(luò)包的處理是不區(qū)分應(yīng)用的堡掏。從四層再往上,就需要區(qū)分網(wǎng)絡(luò)包發(fā)給哪個應(yīng)用刨疼。在傳輸層的 TCP 和 UDP 協(xié)議里面泉唁,都有端口的概念,不同的應(yīng)用監(jiān)聽不同的端口揩慕。

網(wǎng)絡(luò)調(diào)用時通過操作系統(tǒng)的socket來進行的亭畜。在網(wǎng)絡(luò)協(xié)議紙上,用戶態(tài)的應(yīng)用層和內(nèi)核態(tài)進行互通就需要借助系統(tǒng)調(diào)用socket來進行迎卤。網(wǎng)絡(luò)分完層之后贱案,對于數(shù)據(jù)包的發(fā)送,就是層層封裝的過程止吐。

如圖在 Linux 服務(wù)器 B 上部署的服務(wù)端 Nginx 和 Tomcat宝踪,都是通過 Socket 監(jiān)聽 80 和 8080 端口。這個時候碍扔,內(nèi)核的數(shù)據(jù)結(jié)構(gòu)就知道了瘩燥。如果遇到發(fā)送到這兩個端口的,就發(fā)送給這兩個進程不同。在 Linux 服務(wù)器 A 上的客戶端厉膀,打開一個 Firefox 連接 Ngnix。也是通過 Socket二拐,客戶端會被分配一個隨機端口 12345服鹅。同理,打開一個 Chrome 連接 Tomcat百新,同樣通過 Socket 分配隨機端口 12346企软。

undefined

在客戶端瀏覽器,我們將請求封裝為 HTTP 協(xié)議饭望,通過 Socket 發(fā)送到內(nèi)核仗哨。內(nèi)核的網(wǎng)絡(luò)協(xié)議棧里面形庭,在 TCP 層創(chuàng)建用于維護連接、序列號厌漂、重傳萨醒、擁塞控制的數(shù)據(jù)結(jié)構(gòu),將 HTTP 包加上 TCP 頭苇倡,發(fā)送給 IP 層富纸,IP 層加上 IP 頭,發(fā)送給 MAC 層旨椒,MAC 層加上 MAC 頭胜嗓,從硬件網(wǎng)卡由數(shù)字信號轉(zhuǎn)化成光或電信號發(fā)出去

交換機:網(wǎng)絡(luò)包會先到達網(wǎng)絡(luò)1的交換機钩乍。或稱二層設(shè)備,這是因為怔锌,交換機只會處理到第二層寥粹,然后它會將網(wǎng)絡(luò)包的 MAC 頭拿下來,看看該網(wǎng)絡(luò)包的目標(biāo)MAC在該網(wǎng)絡(luò)內(nèi)的出口是哪里埃元,對應(yīng)關(guān)系都記憶在了交換機中ARP表中涝涤。發(fā)現(xiàn)目標(biāo)MAC是在自己右面的網(wǎng)口,于是就從這個網(wǎng)口發(fā)出去岛杀。

路由器:網(wǎng)絡(luò)包會到達中間的 Linux 路由器阔拳,它左面的網(wǎng)卡會收到網(wǎng)絡(luò)包,發(fā)現(xiàn) MAC 地址匹配类嗤,就交給 IP 層糊肠,在 IP 層根據(jù) IP 頭中的信息,在路由表中查找遗锣。下一跳在哪里货裹,應(yīng)該從哪個網(wǎng)口發(fā)出去。路由器也稱為三層設(shè)備精偿,因為它只會處理到第三層弧圆。

最終網(wǎng)絡(luò)包會被轉(zhuǎn)發(fā)到 Linux 服務(wù)器 B,它發(fā)現(xiàn) MAC 地址匹配笔咽,就將 MAC 頭取下來搔预,交給上一層。IP 層發(fā)現(xiàn) IP 地址匹配叶组,將 IP 頭取下來拯田,交給上一層。TCP 層會根據(jù) TCP 頭中的序列號等信息甩十,發(fā)現(xiàn)它是一個正確的網(wǎng)絡(luò)包勿锅,就會將網(wǎng)絡(luò)包++緩存起來++帕膜,等待應(yīng)用層的讀取

應(yīng)用層通過 Socket 監(jiān)聽某個端口溢十,因而讀取的時候垮刹,內(nèi)核會根據(jù) TCP 頭中的端口號,將網(wǎng)絡(luò)包發(fā)給相應(yīng)的應(yīng)用张弛。HTTP 層的頭和正文荒典,是應(yīng)用層來解析的。當(dāng)應(yīng)用層處理完 HTTP 的請求吞鸭,會將結(jié)果仍然封裝為 HTTP 的網(wǎng)絡(luò)包寺董,通過 Socket 接口,發(fā)送給內(nèi)核刻剥。

內(nèi)核會經(jīng)過層層封裝遮咖,從物理網(wǎng)口發(fā)送出去,經(jīng)過網(wǎng)絡(luò) 2 的交換機造虏,Linux 路由器到達網(wǎng)絡(luò) 1御吞,經(jīng)過網(wǎng)絡(luò) 1 的交換機,到達 Linux 服務(wù)器 A漓藕。在 Linux 服務(wù)器 A 上陶珠,經(jīng)過層層解封裝,通過 socket 接口享钞,根據(jù)客戶端的隨機端口號揍诽,發(fā)送給客戶端的應(yīng)用程序,瀏覽器栗竖。于是瀏覽器就能夠顯示出一個絢麗多彩的頁面了暑脆。

集線器&交換機區(qū)別:總的來說是,廣播和記憶分發(fā)狐肢,減少局域網(wǎng)鏈路中廣播消息沖突饵筑。最開始的網(wǎng)絡(luò)設(shè)備主要有集線器,交換器处坪,路由器根资,分為對應(yīng)著一層設(shè)備,二層設(shè)備同窘,三層設(shè)備玄帕,可以想象一下,集線器和交換器是把多個獨立的計算機連接在一個網(wǎng)路中想邦,然后通過路由器連接多個網(wǎng)絡(luò)來形成目前的主流的網(wǎng)絡(luò)拓撲結(jié)構(gòu)裤纹,在集線器和交換器上面有多個網(wǎng)口,這樣多個計算機就可以簡單的通過網(wǎng)線分別連接某一個網(wǎng)口就可以組成一個局域網(wǎng)來進行彼此交流,這個時候集線器工作機制很簡單鹰椒,就是簡單的進行廣播锡移,這樣所有的計算機都會收到,但是只有一個會進行回復(fù)漆际,但是這種方式會造成鏈路中的消息的沖突嚴(yán)重從而影響我們交流的效率淆珊,尤其是如果計算機越來越多就更嚴(yán)重了,所以這個使用就產(chǎn)生了交換器奸汇,交換器有記憶功能施符,會根據(jù)消息的發(fā)送情況動態(tài)的進行學(xué)習(xí),記錄哪一個網(wǎng)口對應(yīng)的計算機的mac地址擂找,這樣一來當(dāng)再次發(fā)送的時候戳吝,只需要查詢一下找到對應(yīng)的網(wǎng)口進行直接交付就可以完成交流,從而避免了大量的廣播消息沖突贯涎。這樣通過集線器和交換器就可以完成在一個網(wǎng)絡(luò)(同一個局域網(wǎng))中的機器之間的通信听哭。

Socket

socket 接口大多數(shù)情況下操作的是傳輸層,即兩個主流的協(xié)議 TCP 和 UDP塘雳,更底層的協(xié)議不用它來操心陆盘。從本質(zhì)上來講,所謂的建立連接粉捻,其實是為了在客戶端和服務(wù)端維護連接,而建立一定的數(shù)據(jù)結(jié)構(gòu)來維護雙方交互的狀態(tài)斑芜,并用這樣的數(shù)據(jù)結(jié)構(gòu)來保證面向連接的特性肩刃。所謂的連接,就是兩端數(shù)據(jù)結(jié)構(gòu)狀態(tài)的協(xié)同杏头,兩邊的狀態(tài)能夠?qū)Φ蒙嫌7蟃CP協(xié)議的規(guī)則,就認為連接存在醇王;兩面狀態(tài)對不上呢燥,連接就算斷了。 數(shù)據(jù)結(jié)構(gòu)屬性的協(xié)同寓娩。

流量控制和擁塞控制其實就是根據(jù)收到的對端的網(wǎng)絡(luò)包叛氨,調(diào)整兩端數(shù)據(jù)結(jié)構(gòu)的狀態(tài)。TCP 協(xié)議的設(shè)計理論上認為棘伴,這樣調(diào)整了數(shù)據(jù)結(jié)構(gòu)的狀態(tài)寞埠,就能進行流量控制和擁塞控制了。所謂的可靠焊夸,也是兩端的數(shù)據(jù)結(jié)構(gòu)做的事情仁连。不丟失其實是數(shù)據(jù)結(jié)構(gòu)在“點名”,順序到達其實是數(shù)據(jù)結(jié)構(gòu)在“排序”阱穗,面向數(shù)據(jù)流其實是數(shù)據(jù)結(jié)構(gòu)將零散的包饭冬,按照順序捏成一個流發(fā)給應(yīng)用層使鹅。

TCP和UDP的區(qū)別:

  • TCP 是面向連接的,UDP 是面向無連接的昌抠。
  • TCP 提供可靠交付患朱,無差錯、不丟失扰魂、不重復(fù)麦乞、并且按序到達;UDP 不提供可靠交付劝评,不保證不丟失姐直,不保證按順序到達。
  • TCP 是面向字節(jié)流的蒋畜,發(fā)送時發(fā)的是一個流声畏,沒頭沒尾;UDP 是面向數(shù)據(jù)報的姻成,一個一個地發(fā)送插龄。
  • TCP 是可以提供流量控制和擁塞控制的,既防止對端被壓垮科展,也防止網(wǎng)絡(luò)被壓垮均牢。

無論是用 socket 操作 TCP,還是 UDP才睹,首先都要調(diào)用 socket 函數(shù)徘跪。socket 函數(shù)用于創(chuàng)建一個 socket 的文件描述符,唯一標(biāo)識一個 socket琅攘。 通信結(jié)束后垮庐,我們還要像關(guān)閉文件一樣,關(guān)閉 socket坞琴。

int socket(int domain, int type, int protocol);

socket 函數(shù)有三個參數(shù):

  1. domain:表示使用什么 IP 層協(xié)議哨查。AF_INET 表示 IPv4,AF_INET6 表示 IPv6剧辐。
  2. type:表示 socket 類型寒亥。SOCK_STREAM,顧名思義就是 TCP 面向流的荧关,SOCK_DGRAM 就是 UDP 面向數(shù)據(jù)報的护盈,SOCK_RAW 可以直接操作 IP 層,或者非 TCP 和 UDP 的協(xié)議羞酗。例如 ICMP腐宋。
  3. protocol 表示的協(xié)議,包括 IPPROTO_TCP、IPPTOTO_UDP胸竞。

面向TCP:


undefined

TCP 的服務(wù)端要先監(jiān)聽一個端口欺嗤,一般是先調(diào)用 bind 函數(shù),給這個 socket 賦予一個端口和 IP 地址卫枝。服務(wù)端所在的服務(wù)器可能有多個網(wǎng)卡煎饼、多個地址,可以選擇監(jiān)聽在一個地址校赤,也可以監(jiān)聽 0.0.0.0 表示所有的地址都監(jiān)聽吆玖, 客戶端要訪問服務(wù)端,肯定事先要知道服務(wù)端的端口马篮。客戶端不需要 bind沾乘,因為瀏覽器,隨機分配一個端口就可以了浑测,只有你主動去連接別人翅阵,別人不會主動連接你,沒有人關(guān)心客戶端監(jiān)聽到了哪里迁央。

如果在網(wǎng)絡(luò)上傳輸超過 1 Byte 的類型掷匠,就要區(qū)分大端(Big Endian)和小端。最低位放在最后一個位置岖圈,我們叫作小端讹语,最低位放在第一個位置,叫作大端蜂科。TCP/IP 棧是按照大端來設(shè)計的顽决,而 x86 機器多按照小端來設(shè)計,因而發(fā)出去時需要做一個轉(zhuǎn)換崇摄。

TCP鏈接:三次握手擎值,其實就是將客戶端和服務(wù)端的狀態(tài)通過三次網(wǎng)絡(luò)交互慌烧,達到初始狀態(tài)是協(xié)同的狀態(tài)逐抑。

undefined

服務(wù)端要調(diào)用 listen 進入 LISTEN 狀態(tài),等待客戶端進行連接屹蚊。

int listen(int sockfd, int backlog);

連接的建立過程厕氨,也即三次握手,是 TCP 層的動作汹粤,是在內(nèi)核完成的命斧,應(yīng)用層不需要參與

接著服務(wù)端只需要調(diào)用 accept嘱兼,等待內(nèi)核完成了至少一個連接的建立国葬,才返回。如果沒有一個連接完成了三次握手,accept 就一直等待汇四;如果有多個客戶端發(fā)起連接接奈,并且在內(nèi)核里面完成了多個三次握手,建立了多個連接通孽,++這些連接會被放在一個隊列里面++序宦。++accept 會從隊列里面取出一個來進行處理。如果想進一步處理其他連接背苦,需要調(diào)用多次 accept互捌,所以 accept 往往在一個循環(huán)里面++

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

接下來行剂,客戶端可以通過 connect 函數(shù)發(fā)起連接秕噪。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

在參數(shù)中指明要連接的IP地址和端口號,然后發(fā)起三次握手硼讽。內(nèi)核會給客戶端分配一個臨時的端口巢价。一旦握手成功,服務(wù)端的 accept 就會返回另一個 socket固阁。這里需要注意的是壤躲,服務(wù)端監(jiān)聽的 socket 和真正用來傳送數(shù)據(jù)的 socket,是兩個 socket备燃,一個叫作監(jiān)聽socket碉克,一個叫作已連接socket。仔細一想這兩個也必然不能為一個socket并齐,功能和對應(yīng)數(shù)據(jù)結(jié)構(gòu)中的內(nèi)容也不同漏麦。一個負責(zé)為服務(wù)端接受各種客戶端發(fā)來的鏈接,一個負責(zé)為服務(wù)端和對應(yīng)不同客戶端的通信工作况褪。成功連接建立之后撕贞,雙方開始通過read和write函數(shù)來讀寫數(shù)據(jù),就像往一個文件流里面寫東西一樣测垛。

對于UDP:


undefined

UDP 是沒有連接的捏膨,所以不需要三次握手,也就不需要調(diào)用 listen 和 connect食侮,但是 UDP 的交互仍然需要 IP 地址和端口號号涯,因而也需要 bind。 對于 UDP 來講锯七,沒有所謂的連接維護链快,也沒有所謂的連接的發(fā)起方和接收方,甚至都不存在客戶端和服務(wù)端的概念眉尸,大家就都是客戶端域蜗,也同時都是服務(wù)端巨双。

只要有一個 socket,多臺機器就可以任意通信霉祸,不存在哪兩臺機器是屬于一個連接的概念炉峰。因此,每一個 UDP 的 socket 都需要 bind脉执。每次通信時疼阔,調(diào)用 sendto 和 recvfrom,都要傳入 IP 地址和端口半夷。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

整個交互過程:

  • 服務(wù)端和客戶端都調(diào)用 socket婆廊,得到文件描述符;
  • 服務(wù)端調(diào)用 listen巫橄,進行監(jiān)聽淘邻;
  • 服務(wù)端調(diào)用 accept,等待客戶端連接湘换;
  • 客戶端調(diào)用 connect宾舅,連接服務(wù)端;
  • 服務(wù)端 accept 返回用于傳輸?shù)?socket 的文件描述符彩倚;
  • 客戶端調(diào)用 write 寫入數(shù)據(jù)筹我;
  • 服務(wù)端調(diào)用 read 讀取數(shù)據(jù)。
undefined

發(fā)送網(wǎng)絡(luò)包

VFS->IP

MSS:Max Segment Size帆离,最大分段大小
MTU:Maximum Transmission Unit蔬蕊,最大傳輸單元
cwnd:congestion window,擁塞窗口
slow start哥谷,慢啟動
rwnd:receive window岸夯,滑動窗口

調(diào)用系統(tǒng)調(diào)用write函數(shù)后:聲明了一個變量copied,初始化為0们妥,這表示拷貝了多少數(shù)據(jù)猜扮。如果用戶的數(shù)據(jù)沒有發(fā)送完畢,就一直循環(huán)监婶。循環(huán)里聲明了一個copy變量旅赢,表示這次拷貝的數(shù)值,在循環(huán)的最后有copied+=copy压储,將每次拷貝的數(shù)量都加起來鲜漩。用來記錄用戶發(fā)送了多少數(shù)據(jù)源譬。接著:

  1. tcp_write_queue_tail 從 TCP 寫入隊列 sk_write_queue 中拿出最后一個 struct sk_buff集惋,在這個寫入隊列中排滿了要發(fā)送的 struct sk_buff,這里面只有最后一個踩娘,才會因為上次用戶給的數(shù)據(jù)太少刮刑,而沒有填滿喉祭。
  2. tcp_send_mss 會計算 MSS纵刘。即在網(wǎng)絡(luò)上傳輸?shù)木W(wǎng)絡(luò)包的大小是有限制的愤惰,而這個限制在最底層開始就有
    1. MTU是二層的一個定義距辆。以以太網(wǎng)為例翘紊,MTU 為 1500 個 Byte蔽氨,前面有 6 個 Byte 的目標(biāo) MAC 地址,6 個 Byte 的源 MAC 地址帆疟,2 個 Byte 的類型鹉究,后面有 4 個 Byte 的 CRC 校驗,共 1518 個 Byte踪宠。
    2. 在 IP 層自赔,一個 IP 數(shù)據(jù)報在以太網(wǎng)中傳輸,如果它的長度大于該 MTU 值柳琢,就要進行分片傳輸绍妨。在 TCP 層有個 MSS,等于 MTU 減去 IP 頭柬脸,再減去 TCP 頭他去。也就是,在不分片的情況下倒堕,TCP 里面放的最大內(nèi)容孤页。
  3. tcp_write_xmit 發(fā)送網(wǎng)絡(luò)包:如果發(fā)送的網(wǎng)絡(luò)包非常大,要進行分段涩馆,MSS限制行施。分段這個事情可以由協(xié)議棧代碼在內(nèi)核做,但是缺點是比較費 CPU魂那,另一種方式是延遲到硬件網(wǎng)卡去做蛾号,需要網(wǎng)卡支持對大數(shù)據(jù)包進行自動分段,可以降低 CPU 負載涯雅。在接下來的函數(shù)中算出來要分成多個段鲜结,然后看是在協(xié)議棧的代碼里面分好,還是等待到了底層網(wǎng)卡再分活逆。
  4. cwnd:為了避免拼命發(fā)包精刷,把網(wǎng)絡(luò)塞滿了,定義一個窗口的概念蔗候,在這個窗口之內(nèi)的才能發(fā)送怒允,超過這個窗口的就不能發(fā)送,來控制發(fā)送的頻率锈遥。
    1. 窗口大腥沂隆:一開始的窗口只有一個 mss 大小叫作 cwnd勘畔。一開始的增長速度的很快的,翻倍增長丽惶。一旦到達一個臨界值 ssthresh炫七,就變成線性增長,我們就稱為擁塞避免钾唬。 但只有出現(xiàn)真正丟包的時候万哪,才算真正擁塞的時候。而一旦丟包抡秆,兩種處理方式
      1. 馬上降回到一個mss壤圃,然后重復(fù)先翻倍再線性對的過程(有點兒無腦)。
      2. 第二種方式降到當(dāng)前 cwnd 的一半琅轧,然后進行線性增長(還可以)伍绳。
    2. 在代碼中,tcp_cwnd_test 會將當(dāng)前的 snd_cwnd乍桂,減去已經(jīng)在窗口里面尚未發(fā)送完畢的網(wǎng)絡(luò)包冲杀,那就是剩下的可用的窗口大小cwnd_quota,也即就能發(fā)送這么多了睹酌。
  5. receive window滑動窗口rwnd:擁塞窗口是為了怕把網(wǎng)絡(luò)塞滿权谁,在出現(xiàn)丟包的時候減少發(fā)送速度,那么滑動窗口就是為了怕把接收方塞滿憋沿,而控制發(fā)送速度旺芽。滑動窗口辐啄,其實就是接收方告訴發(fā)送方自己的網(wǎng)絡(luò)包的接收能力采章,超過這個能力,我就受不了了壶辜。
    1. 因為滑動窗口的存在悯舟,將發(fā)送方的緩存分成了四個部分。
      1. 第一部分:發(fā)送了并且已經(jīng)確認的砸民。這部分是已經(jīng)發(fā)送完畢的網(wǎng)絡(luò)包抵怎,這部分沒有用了,可以回收岭参。
      2. 第二部分:發(fā)送了但尚未確認的反惕。這部分,發(fā)送方要等待演侯,萬一發(fā)送不成功姿染,還要重新發(fā)送,所以不能刪除蚌本。
      3. 第三部分:沒有發(fā)送盔粹,但是已經(jīng)等待發(fā)送的。這部分是接收方空閑的能力程癌,可以馬上發(fā)送舷嗡,接收方收得了。
      4. 第四部分:沒有發(fā)送嵌莉,并且暫時還不會發(fā)送的进萄。這部分已經(jīng)超過了接收方的接收能力,再發(fā)送接收方就不了了锐峭。
        undefined
    2. 因為滑動窗口的存在中鼠,接收方的緩存也要分成了三個部分。在網(wǎng)絡(luò)包的交互過程中沿癞,接收方會將第二部分的大小援雇,作為 AdvertisedWindow 發(fā)送給發(fā)送方,發(fā)送方就可以根據(jù)他來調(diào)整發(fā)送速度了椎扬。
      1. 第一部分:接受并且確認過的任務(wù)惫搏。這部分完全接收成功了,可以交給應(yīng)用層了蚕涤。
      2. 第二部分:還沒接收筐赔,但是馬上就能接收的任務(wù)。這部分有的網(wǎng)絡(luò)包到達了揖铜,但是還沒確認茴丰,不算完全完畢,有的還沒有到達天吓,那就是接收方能夠接受的最大的網(wǎng)絡(luò)包數(shù)量贿肩。
      3. 第三部分:還沒接收,也沒法接收的任務(wù)龄寞。這部分已經(jīng)超出接收方能力尸曼。
  6. tcp_transmit_skb,真的去發(fā)送一個網(wǎng)絡(luò)包:填充 TCP 頭


    undefined

IP->MAC

FIB:Forwarding Information Base萄焦,轉(zhuǎn)發(fā)信息表即路由表

ip_queue_xmit 函數(shù)開始

  1. 選取路由控轿,也即我要發(fā)送這個包應(yīng)該從哪個網(wǎng)卡出去,依據(jù)FIB拂封。路由表可以有多個茬射,一般會有一個主表
    1. 路由:路由就是在 Linux 服務(wù)器上的路由表里面配置的一條一條規(guī)則。這些規(guī)則大概是這樣的:想訪問某個網(wǎng)段冒签,從某個網(wǎng)卡出去在抛,下一跳是某個 IP
    2. 機器的路由表通過 ip route 命令查看。
      # Linux服務(wù)器A
      default via 192.168.1.1 dev eth0
      192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 metric 100
      
      # Linux服務(wù)器B
      default via 192.168.2.1 dev eth0
      192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.100 metric 100
      
      # Linux服務(wù)器做路由器
      192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1  
      192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
      
undefined

對于兩端的服務(wù)器來講萧恕,我們沒有太多路由可以選刚梭,但是對于中間的 Linux 服務(wù)器做路由器來講肠阱,這里有兩條路可以選,一個是往左面轉(zhuǎn)發(fā)朴读,一個是往右面轉(zhuǎn)發(fā)屹徘,就需要路由表的查找。因為路由表要按照前綴進行查詢衅金,希望找到最長匹配的那一個噪伊,例如 192.168.2.0/24 和 192.168.0.0/16 都能匹配 192.168.2.100/24。但是氮唯,基于匹配規(guī)則應(yīng)該使用 192.168.2.0/24 的這一條鉴吹。基本原理是Trie樹結(jié)構(gòu)。找到了路由惩琉,就知道了應(yīng)該從哪個網(wǎng)卡發(fā)出去豆励。

  1. 準(zhǔn)備 IP 層的頭,往里面填充內(nèi)容瞒渠。


    undefined
  • 標(biāo)識位里面設(shè)置是否允許分片 frag_off肆糕。如果不允許,而遇到 MTU 太小過不去的情況在孝,就發(fā)送 ICMP 報錯诚啃。
  • TTL 是這個包的存活時間,為了防止一個 IP 包迷路以后一直存活下去私沮,每經(jīng)過一個路由器 TTL 都減一始赎,減為零則“死去”。
  • 設(shè)置 protocol仔燕,指的是更上層的協(xié)議造垛,這里是 TCP。
  1. 調(diào)用 ip_local_out 發(fā)送 IP 包晰搀。內(nèi)核如果需要通過某個網(wǎng)絡(luò)接口發(fā)送數(shù)據(jù)包五辽,都需要按照為這個接口配置的 qdisc(排隊規(guī)則)把數(shù)據(jù)包加入隊列
    • 最簡單的qdisc是pfifo外恕,它不對進入的數(shù)據(jù)包做任何的處理杆逗,數(shù)據(jù)包采用先入先出的方式通過隊列。
    • pfifo_fast 稍微復(fù)雜一些鳞疲,它的隊列包括三個波段(band)罪郊。在每個波段里面,使用先進先出規(guī)則尚洽。三個波段的優(yōu)先級也不相同悔橄。band 0 的優(yōu)先級最高,band 2 的最低。如果 band 0 里面有數(shù)據(jù)包癣疟,系統(tǒng)就不會處理 band 1 里面的數(shù)據(jù)包挣柬,band 1 和 band 2 之間也是一樣。數(shù)據(jù)包是按照服務(wù)類型(Type of Service睛挚,TOS)被分配到三個波段里面的邪蛔。TOS 是 IP 頭里面的一個字段,代表了當(dāng)前的包是高優(yōu)先級的竞川,還是低優(yōu)先級的店溢。
  1. 后續(xù)真正從隊列中取出網(wǎng)絡(luò)包進行發(fā)送的函數(shù)叁熔,如果發(fā)送不成功委乌,會返回 NETDEV_TX_BUSY。這說明網(wǎng)絡(luò)卡很忙荣回,會重新放入隊列遭贸。隊列中的網(wǎng)絡(luò)包取出后如果成功分發(fā),是會被送到設(shè)備驅(qū)動層的心软。對應(yīng)處理函數(shù)壕吹,會得到這個網(wǎng)卡對應(yīng)的適配器,然后將其放入硬件網(wǎng)卡的隊列中删铃。

接收網(wǎng)絡(luò)包

網(wǎng)卡作為一個硬件耳贬,接收到網(wǎng)絡(luò)包,會觸發(fā)一個中斷通知操作系統(tǒng)猎唁。但為了防止CPU被無休止的中斷打斷咒劲,當(dāng)一些網(wǎng)絡(luò)包到來觸發(fā)了中斷,內(nèi)核處理完這些網(wǎng)絡(luò)包之后诫隅,我們可以先進入主動輪詢poll網(wǎng)卡的方式腐魂,主動去接收到來的網(wǎng)絡(luò)包。如果一直有逐纬,就一直處理蛔屹,等處理告一段落,就返回干其他的事情豁生。當(dāng)再有下一批網(wǎng)絡(luò)包到來的時候兔毒,再中斷,再輪詢poll甸箱。這樣就會大大減少中斷的數(shù)量眼刃,提升網(wǎng)絡(luò)處理的效率,這種處理方式我們稱為 NAPI摇肌。

網(wǎng)卡驅(qū)動程序初始化的時候會注冊一個驅(qū)動擂红,并創(chuàng)建一個struct net_device 表示這個網(wǎng)絡(luò)設(shè)備,并且會為這個網(wǎng)絡(luò)設(shè)備注冊一個輪詢 poll 函數(shù)。將來一旦出現(xiàn)網(wǎng)絡(luò)包的時候昵骤,就是要通過它來輪詢了树碱。當(dāng)一個網(wǎng)卡被激活的時候,會在這里面注冊一個硬件的中斷處理函數(shù)变秦。

如果一個網(wǎng)絡(luò)包到來成榜,觸發(fā)了硬件中斷。會觸發(fā)相關(guān)的中斷處理函數(shù)蹦玫,當(dāng)它被調(diào)用的時候赎婚,中斷是暫時關(guān)閉的。主要做的事情是有一個循環(huán)樱溉,在 poll_list 里面取出網(wǎng)絡(luò)包到達的設(shè)備挣输,然后調(diào)用 napi_poll 來輪詢這些設(shè)備。在網(wǎng)絡(luò)設(shè)備的驅(qū)動層福贞,有一個用于接收網(wǎng)絡(luò)包的 rx_ring撩嚼。它是一個環(huán),從網(wǎng)卡硬件接收的包會放在這個環(huán)里面挖帘。這個環(huán)里面的 buffer_info[]是一個數(shù)組完丽,存放的是網(wǎng)絡(luò)包的內(nèi)容。

通過檢索網(wǎng)絡(luò)包中二層的頭里面的protocol字段來判斷這是一個什么包拇舀。如果是個IP包逻族,則后續(xù)會調(diào)用IP包對應(yīng)的處理函數(shù)。這些函數(shù)中會判斷如果IP進行了分段骄崩,就重新進行組合聘鳞。至此進入第三層。

具體流程:

  1. 硬件網(wǎng)卡接收到網(wǎng)絡(luò)包之后刁赖,通過 DMA 技術(shù)搁痛,將網(wǎng)絡(luò)包放入 Ring Buffer。
  2. 硬件網(wǎng)卡通過中斷通知CPU新的網(wǎng)絡(luò)包的到來宇弛。
  3. 網(wǎng)卡驅(qū)動程序會注冊中斷處理函數(shù) ixgb_intr鸡典。
  4. 中斷處理函數(shù)處理完需要暫時屏蔽中斷的核心流程之后,通過軟中斷 NET_RX_SOFTIRQ 觸發(fā)接下來的處理過程枪芒。
  5. NET_RX_SOFTIRQ 軟中斷處理函數(shù) net_rx_action彻况,net_rx_action 會調(diào)用 napi_poll,進而調(diào)用 ixgb_clean_rx_irq舅踪,從 Ring Buffer 中讀取數(shù)據(jù)到內(nèi)核 struct sk_buff纽甘。
  6. 調(diào)用 netif_receive_skb 進入內(nèi)核網(wǎng)絡(luò)協(xié)議棧,進行一些關(guān)于 VLAN 的二層邏輯處理后抽碌,調(diào)用 ip_rcv 進入三層 IP 層悍赢。
  7. 在 IP 層,會處理 iptables 規(guī)則,然后調(diào)用 ip_local_deliver左权,交給更上層 TCP 層皮胡。
  8. 在 TCP 層調(diào)用 tcp_v4_rcv。

tcp_v4_rcv 中赏迟,得到 TCP 的頭屡贺,TCP 層是分狀態(tài)的,狀態(tài)被維護在數(shù)據(jù)結(jié)構(gòu) struct sock 里面锌杀,要根據(jù) IP 地址以及 TCP 頭里面的內(nèi)容甩栈,找到這個包對應(yīng)的 struct sock,從而得到這個包對應(yīng)的連接的狀態(tài)糕再。后續(xù)需要根據(jù)不同的狀態(tài)做不同的處理量没,CP_LISTEN、TCP_NEW_SYN_RECV 狀態(tài)屬于連接建立過程中亿鲜。TCP_TIME_WAIT 狀態(tài)是連接結(jié)束的時候的狀態(tài)允蜈。

后續(xù)處理函數(shù)對于收到的網(wǎng)絡(luò)包冤吨,要分情況進行處理蒿柳。

  1. seq == tp->rcv_nxt,說明來的網(wǎng)絡(luò)包正是我服務(wù)端期望的下一個網(wǎng)絡(luò)包漩蟆。這個時候我們判斷用戶進程是否也是正在等待讀取垒探,這種情況下,就直接將網(wǎng)絡(luò)包拷貝給用戶進程就可以了怠李。如果用戶進程沒有正在等待讀取圾叼,或者因為內(nèi)存原因沒有能夠拷貝成功,會將網(wǎng)絡(luò)包放入 sk_receive_queue 隊列捺癞。

接下來夷蚊,當(dāng)前的網(wǎng)絡(luò)包接收成功后,更新下一個期待的網(wǎng)絡(luò)包序號髓介。然后檢測亂序隊列中的包會不會因為這個新的網(wǎng)絡(luò)包的到來惕鼓,也能放入到 sk_receive_queue 隊列中。亂序的包不能進入 sk_receive_queue 隊列唐础。因為一旦進入到這個隊列箱歧,意味著可以發(fā)送給用戶進程。 按照 TCP 的定義一膨,用戶進程應(yīng)該是按順序收到包的呀邢,沒有排好序,就不能給用戶進程豹绪。所以提前與當(dāng)前序列到達的序列號更大的網(wǎng)絡(luò)包會暫存到亂序隊列中价淌,直到前置網(wǎng)絡(luò)包到達后,并且檢測亂序隊列的時候,才能將暫存于此的網(wǎng)絡(luò)包放入sk_receive_queue 隊列蝉衣。

  1. 服務(wù)端期望網(wǎng)絡(luò)包 5豺型。但是,來了一個網(wǎng)絡(luò)包3买乃。情況是:那客戶端就認為網(wǎng)絡(luò)包 3 沒有發(fā)送成功姻氨,于是又發(fā)送了一遍,這種情況下剪验,要趕緊給客戶端再發(fā)送一次 ACK肴焊,表示早就收到了。
  1. seq 不小于 rcv_nxt + tcp_receive_window功戚。這說明客戶端發(fā)送得太猛了娶眷。本來 seq 肯定應(yīng)該在接收窗口里面的,這樣服務(wù)端才來得及處理啸臀,結(jié)果現(xiàn)在超出了接收窗口届宠,說明客戶端一下子把服務(wù)端給塞滿了。這種情況下乘粒,服務(wù)端不能再接收數(shù)據(jù)包了豌注,只能發(fā)送 ACK了,在ACK中會將接收窗口為0的情況告知客戶端灯萍,客戶端就知道不能再發(fā)送了轧铁。這個時候雙方只能交互窗口探測數(shù)據(jù)包,直到服務(wù)端因為用戶進程把數(shù)據(jù)讀走了旦棉,空出接收窗口齿风,才能在ACK里面再次告訴客戶端,又有窗口了绑洛,又能發(fā)送數(shù)據(jù)包了救斑。
  1. seq 小于 rcv_nxt,但是 end_seq 大于 rcv_nxt真屯,這說明從 seq 到 rcv_nxt 這部分網(wǎng)絡(luò)包原來的 ACK 客戶端沒有收到脸候,所以重新發(fā)送了一次,從 rcv_nxt 到 end_seq 時新發(fā)送的讨跟,可以放入 sk_receive_queue 隊列纪他。

當(dāng)前四種情況都排除掉了,說明網(wǎng)絡(luò)包一定是一個亂序包了晾匠。當(dāng)接收的網(wǎng)絡(luò)包進入各種隊列之后茶袒,接下來就等待用戶進程去讀取它們了。通過系統(tǒng)調(diào)用read來讀取該socket凉馆。

用戶進程在讀取socket的過程中薪寓,是根據(jù)receive_queue 隊列亡资、prequeue 隊列和 backlog 隊列這個優(yōu)先級來進行的。把前一個隊列處理完畢向叉,才處理后一個隊列锥腻。對應(yīng)處理函數(shù) tcp_recvmsg 里面有一個 while 循環(huán),不斷地讀取網(wǎng)絡(luò)包母谎。會先處理 sk_receive_queue 隊列瘦黑。如果找到了網(wǎng)絡(luò)包,將網(wǎng)絡(luò)包拷貝到用戶進程中奇唤,然后直接進入下一層循環(huán)繼續(xù)讀取處理幸斥。最后,哪里都沒有網(wǎng)絡(luò)包咬扇,我們只好調(diào)用 sk_wait_data甲葬,繼續(xù)等待在哪里,等待網(wǎng)絡(luò)包的到來懈贺。

總體順序:

  1. 硬件網(wǎng)卡接收到網(wǎng)絡(luò)包之后经窖,通過 DMA 技術(shù),將網(wǎng)絡(luò)包放入 Ring Buffer梭灿;
  2. 硬件網(wǎng)卡通過中斷通知 CPU 新的網(wǎng)絡(luò)包的到來画侣;
  3. 網(wǎng)卡驅(qū)動程序會注冊中斷處理函數(shù) ixgb_intr;
  4. 中斷處理函數(shù)處理完需要暫時屏蔽中斷的核心流程之后胎源,通過軟中斷 NET_RX_SOFTIRQ 觸發(fā)接下來的處理過程棉钧;
  5. NET_RX_SOFTIRQ 軟中斷處理函數(shù) net_rx_action屿脐,net_rx_action 會調(diào)用 napi_poll涕蚤,進而調(diào)用 ixgb_clean_rx_irq,從 Ring Buffer 中讀取數(shù)據(jù)到內(nèi)核 struct sk_buff的诵;
  6. 調(diào)用 netif_receive_skb 進入內(nèi)核網(wǎng)絡(luò)協(xié)議棧万栅,進行一些關(guān)于 VLAN 的二層邏輯處理后,調(diào)用 ip_rcv 進入三層 IP 層西疤;
  7. 在 IP 層烦粒,會處理 iptables 規(guī)則,然后調(diào)用 ip_local_deliver 交給更上層 TCP 層代赁;
  8. 在 TCP 層調(diào)用 tcp_v4_rcv扰她,這里面有三個隊列需要處理,如果當(dāng)前的 Socket 不是正在被讀芭碍;取徒役,則放入 backlog 隊列,如果正在被讀取窖壕,不需要很實時的話忧勿,則放入 prequeue 隊列杉女,其他情況調(diào)用 tcp_v4_do_rcv;
  9. 在 tcp_v4_do_rcv 中鸳吸,如果是處于 TCP_ESTABLISHED 狀態(tài)熏挎,調(diào)用 tcp_rcv_established,其他的狀態(tài)晌砾,調(diào)用 tcp_rcv_state_process坎拐;
  10. 在 tcp_rcv_established 中,調(diào)用 tcp_data_queue养匈,如果序列號能夠接的上廉白,則放入 sk_receive_queue 隊列;如果序列號接不上乖寒,則暫時放入 out_of_order_queue 隊列猴蹂,等序列號能夠接上的時候,再放入 sk_receive_queue 隊列楣嘁。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末磅轻,一起剝皮案震驚了整個濱河市垂睬,隨后出現(xiàn)的幾起案子上煤,更是在濱河造成了極大的恐慌岛琼,老刑警劉巖恍涂,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诬辈,死亡現(xiàn)場離奇詭異昆咽,居然都是意外死亡金踪,警方通過查閱死者的電腦和手機捺氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門买雾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來把曼,“玉大人,你說我怎么就攤上這事漓穿∴途” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵晃危,是天一觀的道長叙赚。 經(jīng)常有香客問我,道長僚饭,這世上最難降的妖魔是什么震叮? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鳍鸵,結(jié)果婚禮上苇瓣,老公的妹妹穿的比我還像新娘。我一直安慰自己权纤,他們只是感情好钓简,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布乌妒。 她就那樣靜靜地躺著,像睡著了一般外邓。 火紅的嫁衣襯著肌膚如雪撤蚊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天损话,我揣著相機與錄音侦啸,去河邊找鬼。 笑死丧枪,一個胖子當(dāng)著我的面吹牛光涂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拧烦,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼忘闻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了恋博?” 一聲冷哼從身側(cè)響起齐佳,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎债沮,沒想到半個月后炼吴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡疫衩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年硅蹦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闷煤。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡童芹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出曹傀,到底是詐尸還是另有隱情辐脖,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布皆愉,位于F島的核電站,受9級特大地震影響艇抠,放射性物質(zhì)發(fā)生泄漏幕庐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一家淤、第九天 我趴在偏房一處隱蔽的房頂上張望异剥。 院中可真熱鬧,春花似錦絮重、人聲如沸冤寿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽督怜。三九已至殴瘦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間号杠,已是汗流浹背蚪腋。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姨蟋,地道東北人屉凯。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像眼溶,于是被迫代替她去往敵國和親悠砚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345