rculinux
1. 如何1分鍾內對 linux 性能快速分析(113資訊網)
當你在IDC主機商購買一台系統為 Linux 伺服器之後,我想大家第一時間就是對主機進行一個性能分析,這里我跟大家分享幾個命令,能讓大家在一分鍾以內對自己的性能有一個大致的鳥解?
uptime
dmesg | tail
vmstat 1
mpstat -P ALL 1
pidstat 1
iostat -xz 1
free -m
sar -n DEV 1
sar -n TCP,ETCP 1
top
這10個命令到底是什麼意思,我為大家一一解釋一下:
1.uptime
# uptime
03:16:26 up 21:31, 1 user, load average: 10.02, 06.43, 09.02
在上面的例子中,平均負載顯示是在不斷增加的,1 分鍾的值是 10,相比 15 分鍾的值 09 來說是增加了。這個數字這么大就意味著有事情發生了.
2. dmesg | tail
# dmesg | tail
[ 14.102501] ISO 9660 Extensions: RRIP_1991A
[ 15.900216] ISO 9660 Extensions: Microsoft Joliet Level 3
[ 15.900234] ISO 9660 Extensions: RRIP_1991A
[ 17.030540] EXT4-fs (vda1): resizing filesystem from 5242619 to 13106939 blocks
[ 17.151434] random: crng init done
[ 17.151436] random: 7 urandom warning(s) missed e to ratelimiting
[ 18.314268] EXT4-fs (vda1): resized filesystem to 13106939
[ 20.394666] new mount options do not match the existing superblock, will be ignored
[ 38.405804] ISO 9660 Extensions: Microsoft Joliet Level 3
[ 38.407599] ISO 9660 Extensions: RRIP_1991A
這里展示的是最近 10 條系統消息日誌,如果系統消息沒有就不會展示。主要是看由於性能問題導致的錯誤。
3. vmstat 1
# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 324644 141184 1270628 0 0 10 40 207 431 1 1 99 0 0
0 0 0 324388 141184 1270628 0 0 0 0 130 280 1 1 98 0 0
0 0 0 324388 141184 1270628 0 0 0 0 89 169 0 0 100 0 0
0 0 0 324420 141184 1270628 0 0 0 0 118 225 1 0 99 0 0
0 0 0 324420 141184 1270628 0 0 0 32 125 254 0 0 99 1 0
1 1 0 324420 141184 1270628 0 0 0 68 96 171 0 0 96 4 0
0 0 0 324452 141184 1270628 0 0 0 184 127 166 0 1 96 3 0
^C
r: CPU 上的等待運行的可運行進程數。這個指標提供了判斷 CPU 飽和度的數據,因為它不包含 I/O 等待的進程。可解釋為:「r」 的值比 CPU 數大的時候就是飽和的。
free:空閑內存,單位是 k。如果這個數比較大,就說明你還有充足的空閑內存。「free -m」 和下面第 7 個命令,可以更詳細的分析空閑內存的狀態。
si,so:交換進來和交換出去的數據量,如果這兩個值為非 0 值,那麼就說明沒有內存了。
us,sy,id,wa,st:這些是 CPU 時間的分解,是所有 CPU 的平均值。它們是用戶時間,系統時間(內核),空閑,等待 I/O 時間,和被偷的時間(這里主要指其它的客戶,或者使用 Xen,這些客戶有自己獨立的操作域)。
4. mpstat -P ALL 1
# mpstat -P ALL 1
Linux 4.15.0-88-generic (VM-0-17-ubuntu) 06/15/2020 _x86_64_ (1 CPU)
03:33:26 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
03:33:27 AM all 0.00 0.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 99.00
03:33:27 AM 0 0.00 0.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 99.00
這個命令列印各個 CPU 的時間統計,可以看出整體 CPU 的使用是不是均衡的。由於我使用的是1H2G主機看不出區別!
5. pidstat 1
# pidstat 1
Linux 4.15.0-88-generic (VM-0-17-ubuntu) 06/15/2020 _x86_64_ (1 CPU)
03:34:47 AM UID PID %usr %system %guest %wait %CPU CPU Command
03:34:48 AM 0 1120 1.00 0.00 0.00 0.00 1.00 0 sshd
pidstat 命令為每個 CPU 統計信息功能。由於我使用的是1H2G主機看不出區別!
6. iostat -xz 1
# iostat -xz 1
Linux 4.15.0-88-generic (VM-0-17-ubuntu) 06/15/2020 _x86_64_ (1 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
0.67 0.01 0.52 0.29 0.00 98.52
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.22 0.00 0.00 9.64 0.00 0.00 0.00
scd0 0.02 0.00 0.48 0.00 0.00 0.00 0.00 0.00 0.21 0.00 0.00 27.72 0.00 0.19 0.00
vda 0.64 4.07 9.15 40.59 0.00 1.99 0.00 32.85 3.58 2.31 0.01 14.31 9.96 0.24 0.11
avg-cpu: %user %nice %system %iowait %steal %idle
0.00 0.00 0.00 0.00 0.00 100.00
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
r/s, w/s, rkB/s, wkB/s:這些表示設備上每秒鍾的讀寫次數和讀寫的位元組數(單位是k位元組)。這些可以看出設備的負載情況。性能問題可能就是簡單的因為大量的文件載入請求。
await:I/O 等待的平均時間(單位是毫秒)。這是應用程序所等待的時間,包含了等待隊列中的時間和被調度服務的時間。過大的平均等待時間就預示著設備超負荷了或者說設備有問題了。
avgqu-sz:設備上請求的平均數。數值大於 1 可能表示設備飽和了(雖然設備通常都是可以支持並行請求的,特別是在背後掛了多個磁碟的虛擬設備)。
%util:設備利用率。是使用率的百分數,展示每秒鍾設備工作的時間。這個數值大於 60% 則會導致性能很低(可以在 await 中看),當然這也取決於設備特點。這個數值接近 100% 則表示設備飽和了。
7. free -m/h
ubuntu@VM-0-17-ubuntu:~# free -m
total used free shared buff/cache available
Mem: 1833 137 313 5 1381 1506
Swap: 0 0 0
ubuntu@VM-0-17-ubuntu:~$ free -h
total used free shared buff/cache available
Mem: 1.8G 139M 311M 5.8M 1.3G 1.5G
Swap: 0B 0B 0B
這個命令我相信大家都熟悉,buffers:用於塊設備 I/O 緩沖的緩存,cached:用於文件系統的頁緩存。
8. sar -n DEV 1
ubuntu@VM-0-17-ubuntu:~# sar -n DEV 1
Linux 4.15.0-88-generic (VM-0-17-ubuntu) 06/15/2020 _x86_64_ (1 CPU)
03:43:35 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
03:43:36 AM eth0 11.00 10.00 0.79 1.06 0.00 0.00 0.00 0.00
03:43:36 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
使用這個工具是可以檢測網路介面的吞吐:rxkB/s 和 txkB/s,作為收發數據負載的度量,也是檢測是否達到收發極限。在上面這個例子中,eth0 接收數據達到 0.79 kb 位元組/秒,發送數據達到1.06 位元組/秒。
9. sar -n TCP,ETCP 1
ubuntu@VM-0-17-ubuntu:~# sar -n TCP,ETCP 1
Linux 4.15.0-88-generic (VM-0-17-ubuntu) 06/15/2020 _x86_64_ (1 CPU)
03:49:56 AM active/s passive/s iseg/s oseg/s
03:49:57 AM 0.00 0.00 5.05 3.03
03:49:56 AM atmptf/s estres/s retrans/s isegerr/s orsts/s
03:49:57 AM 0.00 0.00 0.00 0.00 0.00
這是對 TCP 關鍵指標的統計,它包含了以下內容:
active/s:每秒本地發起的 TCP 連接數(例如通過 connect() 發起的連接)。
passive/s:每秒遠程發起的連接數(例如通過 accept() 接受的連接)。
retrans/s:每秒TCP重傳數。
10. top
ubuntu@VM-0-17-ubuntu:~# top
top - 03:53:20 up 1 day, 1:41, 1 user, load average: 0.01, 0.04, 0.00
Tasks: 89 total, 1 running, 52 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1877076 total, 317436 free, 143420 used, 1416220 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 1540856 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3730 root 20 0 105688 6812 5840 S 0.3 0.4 0:00.01 sshd
7546 root 20 0 644608 14924 6776 S 0.3 0.8 2:48.99 YDService
1 root 20 0 159892 9260 6796 S 0.0 0.5 0:06.45 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
7 root 20 0 0 0 0 S 0.0 0.0 0:04.29 ksoftirqd/0
8 root 20 0 0 0 0 I 0.0 0.0 0:08.85 rcu_sched
9 root 20 0 0 0 0 I 0.0 0.0 0:00.00 rcu_bh
10 root rt 0 0 0 0 S 0.0 0.0 0:00.00 migration/0
11 root rt 0 0 0 0 S 0.0 0.0 0:00.16 watchdog/0
12 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/0
13 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kdevtmpfs
top 命令包含了很多我們前面提到的指標。這個命令可以很容易看出指標的變化表示負載的變化,這個看起來和前面的命令有很大不同。
top 的一個缺陷也比較明顯,很難看出變化趨勢,其它像 vmstat 和 pidstat 這樣的工具就會很清晰,它們是以滾動的方式輸出統計信息。所以如果你在看到有問題的信息時沒有及時的暫停下來(Ctrl-S 是暫停, Ctrl-Q 是繼續),那麼這些有用的信息就會被清屏。
文章原文: https://www.113p.cn/129.html (來都來了,就去我博客看下!!)
2. linux rcu鎖問題怎麼查
眾所周知,為了保護共享數空運據,需要一些同步機制,如自旋鎖(spinlock),讀寫鎖(rwlock),它們使用起來非常簡單,而且是一種很有效的同步機制,在UNIX系統和Linux系統中得到了廣泛的使用。但是隨著計算機硬體的快速發展,獲得這種鎖的開銷相對於CPU的速度在成倍地增加,原因很簡單,CPU的速度與訪問內存的速度差距越來越大,而這種鎖使用了原子操作指令,它需要原子地訪問內存,也就說獲得鎖的開銷與訪存速度相關,另外在大部分非x86架構上獲取鎖使用了內存柵(Memory Barrier),這會導致處理器流水線停滯或刷新,因此它的開銷相對於CPU速度而言就越來越大。
在操作系統中,數據一致性訪問是一個非常重要的部分,通常我們可以採用鎖機制實現數據的一致性訪問。例如,semaphore、spinlock機制,在訪問共享數據時,首先訪問鎖資源,在獲取鎖資源的前提下才能實現數據的訪問。這種原理很簡單,根本的思想就是在訪問臨界資源時,首先訪問一個全局的變數(鎖),通過全局變數的狀態來控制線程對臨界資源的訪問。但是,這種思想是需要硬體支持的,硬體需要配合實現全局變數(鎖)的讀-修改-寫,現代CPU都會提供這樣的原子化指令。採用鎖機制實現數據訪問的一致性存在如下兩個問題:
1、 效率問題。鎖機制的實現需要對內存的原子化訪問,這種訪問操作會破壞流水線操作,降低了流水線效率。這是影響性能的一個因素。另外,在採用讀寫鎖機制的情況下,寫鎖是排他鎖,無法實現寫鎖與讀鎖的並發操作,在某些應用下回降低性能。
2、 擴展性問題。當系統中CPU數量增多的時候,採用鎖機制實現數據的同步訪問效率偏低。並且隨著CPU數量的增多,效率降低,由此可見鎖機制實現的數據一致性訪問擴展性差。
為了解決上述問題,Linux中引進了RCU機制。該機制在多CPU的平台上比較適用,對於讀多寫少的應用尤其適游悶用。RCU的思路實際上很簡單,下面對其進行描述:
1、 對於讀操作,可以直接對共享資源進行訪問,但是前提是需要CPU支持訪存操作的原子化,現代CPU對這一點都做了保證。但是RCU的讀操作上下文是不可搶占的(這一點在下面解釋),所以讀訪問共享資源時可以採用read_rcu_lock(),該函數的工作是停止搶占。
2、 對於寫操作,其需要將原來的老數據作一次備份(),然後對備份數據進行修改,修改完畢之後再用新數據更新老數據,更新老數據時採用了rcu_assign_pointer()宏,在該函數中首先屏障一下memory,然後修改老數據。這個操作完成之後,需要進行老數據資源的回收。操作線程向系統注冊回收方法,等待回收。採用數據備份的方法可以實現讀者與寫者之間的並發操作,但是不能解決多個寫著之間的同步,所以當存在多個寫者時,需要通過鎖機制對其進行互斥,也就是在同一時刻只能存在一個寫者。
3、 在RCU機制中存在一個垃圾回收的daemon,當共享資源被update之後,可以採用該daemon實現老數據資源的回收。回收時間點就是在update之前的所有的讀者全部退出。由此可見寫者在update之後是需要睡眠等待的,需斗磨梁要等待讀者完成操作,如果在這個時刻讀者被搶占或者睡眠,那麼很可能會導致系統死鎖。因為此時寫者在等待讀者,讀者被搶占或者睡眠,如果正在運行的線程需要訪問讀者和寫者已經佔用的資源,那麼死鎖的條件就很有可能形成了。
3. 一文搞懂 , Linux內核—— 同步管理(下)
上面講的自旋鎖,信號量和互斥鎖的實現,都是使用了原子操作指令。由於原子操作會 lock,當線程在多個 CPU 上爭搶進入臨界區的時候,都會操作那個在多個 CPU 之間共享的數據 lock。CPU 0 操作了 lock,為了數據的一致性,CPU 0 的操作會導致其他 CPU 的 L1 中的 lock 變成 invalid,在隨後的來自其他 CPU 對 lock 的訪問會導致 L1 cache miss(更准確的說是communication cache miss),必須從下一個 level 的 cache 中獲取。
這就會使緩存一致性變得很糟,導致性能下降。所以內核提供一種新的同步方式:RCU(讀-復制-更新)。
RCU 解決了什麼
RCU 是讀寫鎖的高性能版本,它的核心理念是讀者訪問的同時,寫者可以更新訪問對象的副本,但寫者需要等待所有讀者完成訪問之後,才能刪除老對象。讀者沒有任何同步開銷,而寫者的同步開銷則取決於使用的寫者間同步機制。
RCU 適用於需要頻繁的讀取數據,而相應修改數據並不多的情景,例如在文件系統中,經常需要查找定位目錄,而對目錄的修改相對來說並不多,這就是 RCU 發揮作用的最佳場景。
RCU 例子
RCU 常用的介面如下圖所示:
為了更好的理解,在剖析 RCU 之前先看一個例子:
#include<linux/kernel.h>#include<linux/mole.h>#include<linux/init.h>#include<linux/slab.h>#include<linux/spinlock.h>#include<linux/rcupdate.h>#include<linux/kthread.h>#include<linux/delay.h>structfoo{inta;structrcu_headrcu;};staticstructfoo*g_ptr;staticintmyrcu_reader_thread1(void*data)//讀者線程1{structfoo*p1=NULL;while(1){if(kthread_should_stop())break;msleep(20);rcu_read_lock();mdelay(200);p1=rcu_dereference(g_ptr);if(p1)printk("%s: read a=%d\n",__func__,p1->a);rcu_read_unlock();}return0;}staticintmyrcu_reader_thread2(void*data)//讀者線程2{structfoo*p2=NULL;while(1){if(kthread_should_stop())break;msleep(30);rcu_read_lock();mdelay(100);p2=rcu_dereference(g_ptr);if(p2)printk("%s: read a=%d\n",__func__,p2->a);rcu_read_unlock();}return0;}staticvoidmyrcu_del(structrcu_head*rh)//回收處理操作{structfoo*p=container_of(rh,structfoo,rcu);printk("%s: a=%d\n",__func__,p->a);kfree(p);}staticintmyrcu_writer_thread(void*p)//寫者線程{structfoo*old;structfoo*new_ptr;intvalue=(unsignedlong)p;while(1){if(kthread_should_stop())break;msleep(250);new_ptr=kmalloc(sizeof(structfoo),GFP_KERNEL);old=g_ptr;*new_ptr=*old;new_ptr->a=value;rcu_assign_pointer(g_ptr,new_ptr);call_rcu(&old->rcu,myrcu_del);printk("%s: write to new %d\n",__func__,value);value++;}return0;}staticstructtask_struct*reader_thread1;staticstructtask_struct*reader_thread2;staticstructtask_struct*writer_thread;staticint__initmy_test_init(void){intvalue=5;printk("figo: my mole init\n");g_ptr=kzalloc(sizeof(structfoo),GFP_KERNEL);reader_thread1=kthread_run(myrcu_reader_thread1,NULL,"rcu_reader1");reader_thread2=kthread_run(myrcu_reader_thread2,NULL,"rcu_reader2");writer_thread=kthread_run(myrcu_writer_thread,(void*)(unsignedlong)value,"rcu_writer");return0;}staticvoid__exitmy_test_exit(void){printk("goodbye\n");kthread_stop(reader_thread1);kthread_stop(reader_thread2);kthread_stop(writer_thread);if(g_ptr)kfree(g_ptr);}MODULE_LICENSE("GPL");mole_init(my_test_init);mole_exit(my_test_exit);
執行結果是:
myrcu_reader_thread2:reada=0myrcu_reader_thread1:reada=0myrcu_reader_thread2:reada=0myrcu_writer_thread:writetonew5myrcu_reader_thread2:reada=5myrcu_reader_thread1:reada=5myrcu_del:a=0
RCU 原理
可以用下面一張圖來總結,當寫線程 myrcu_writer_thread 寫完後,會更新到另外兩個讀線程 myrcu_reader_thread1 和 myrcu_reader_thread2。讀線程像是訂閱者,一旦寫線程對臨界區有更新,寫線程就像發布者一樣通知到訂閱者那裡,如下圖所示。
寫者在拷貝副本修改後進行 update 時,首先把舊的臨界資源數據移除(Removal);然後把舊的數據進行回收(Reclamation)。結合 API 實現就是,首先使用 rcu_assign_pointer 來移除舊的指針指向,指向更新後的臨界資源;然後使用 synchronize_rcu 或 call_rcu 來啟動 Reclaimer,對舊的臨界資源進行回收(其中 synchronize_rcu 表示同步等待回收,call_rcu 表示非同步回收)。
為了確保沒有讀者正在訪問要回收的臨界資源,Reclaimer 需要等待所有的讀者退出臨界區,這個等待的時間叫做寬限期(Grace Period)。
Grace Period
中間的黃色部分代表的就是 Grace Period,中文叫做寬限期,從 Removal 到 Reclamation,中間就隔了一個寬限期,只有當寬限期結束後,才會觸發回收的工作。寬限期的結束代表著 Reader 都已經退出了臨界區,因此回收工作也就是安全的操作了。
寬限期是否結束,與 CPU 的執行狀態檢測有關,也就是檢測靜止狀態 Quiescent Status。
Quiescent Status
Quiescent Status,用於描述 CPU 的執行狀態。當某個 CPU 正在訪問 RCU 保護的臨界區時,認為是活動的狀態,而當它離開了臨界區後,則認為它是靜止的狀態。當所有的 CPU 都至少經歷過一次 Quiescent Status 後,寬限期將結束並觸發回收工作。
因為 rcu_read_lock 和 rcu_read_unlock 分別是關閉搶占和打開搶占,如下所示:
staticinlinevoid__rcu_read_lock(void){preempt_disable();}
staticinlinevoid__rcu_read_unlock(void){preempt_enable();}
所以發生搶占,就說明不在 rcu_read_lock 和 rcu_read_unlock 之間,即已經完成訪問或者還未開始訪問。
Linux 同步方式的總結
資料免費領
學習直通車
4. Linux內核中的RCU機制
Linux內核中的RCU機制
RCU的設計思想比較明確,通過新老指針替換的方式來實現免鎖方式的共享保護。但是具體到代碼的層面,理解起來多扒岩少還是會有些困難。下面我准備了關於Linux內核中的RCU機制的文章,提供給大家參考!
RCU讀取側進入臨界區的標志是調用rcu_read_lock,這個函數的代碼是:
static inline void rcu_read_lock(void)
{
__rcu_read_lock();
__acquire(RCU);
rcu_read_acquire();
}
該實現裡面貌似有三個函數調用,但實質性的工作由第一個函數__rcu_read_lock()來完成,__rcu_read_lock()通過調用 preempt_disable()關閉內核可搶占性。但是中斷是允許的,假設讀取者正處於rcu臨界區中且剛讀取了一個共享數據區的指針p(但是還沒有訪問p中的數據成員),發生了一個中斷,而該中斷處理常式ISR恰好需要修改p所指向的數據區,按照RCU的設計原則,ISR會新分配一個同樣大小的數據區new_p,再把老數據區p中的數據拷貝到新數據區,接著是在new_p的基礎上做數據修改的工作(因為是在new_p空間中修改,所以不存在對p的並發訪問,因此說RCU是一種免鎖機制,原因就在這里),ISR在把數據更新的工作完成後,將new_p賦值給p(p=new_p),最後它會再注冊一個回調函數用以在適當的時候釋放老指針p。因此,只要對老指針p上的所有引用都結束了,釋放p就不會有問題。當中斷處理常式做完這些工作返回後,被中斷的進程將依然訪問到p空間上的數據,蘆滑也就是老數據,這樣的結果是RCU機制所允許的。RCU規則對讀取者與寫入者之間因指針切換所造成的短暫的資源視圖不一致問題是允許的。
接下來關於RCU一個有趣的問題是:何時才能釋放老指針。我見過很多書中對此的'回答是:當系統中所有處理器上都發生了一次進程切換。這種程式化的回答常常讓剛接觸RCU機制的讀者感到一頭霧水,為什麼非要等所有處理器上都發生一次進程切換才可以調用回調函數釋放老指針呢?這其實是RCU的設計規則決定的: 所有對老指針的引用只可能發生在rcu_read_lock與rcu_read_unlock所包括的臨界區中,而在這個臨界區中不可能發生進程切換,而一旦出了該臨界區就不應該再有任何形式的對老指針p的引用。很明顯,這個規則要求讀取者在臨界區中不能發生進程切換,因為一旦有進程切換,釋放老指針的回調函數就有可能被調用,從而導致老指針被釋放掉,當被切換掉的進程被重新調度運行時它就有可能引用到一個被釋放掉的內存空間。
現在我們看到為什麼rcu_read_lock只需要關閉內核可搶占性就可以了,因為它使得即便在臨界區中發生了中斷,當前進程也不可能被切換除去。 內核開發者,確切地說,RCU的設計者所能做的只能到這個程度。接下來就是使用者的責任了,如果在rcu的臨界區中調用了一個函數,該函數可能睡眠,那麼RCU的設計規則就遭到了破壞,系統將進入一種不穩定的狀態。
這再次說明,如果想使用一個東西,一定要搞清楚其內在的機制,象上面剛提到的那個例子,即便現在程序不出現問題,但是系統中留下的隱患如同一個定時炸彈, 隨時可能被引爆,尤其是過了很長時間問題才突然爆發出來。絕大多數情形下,找到問題所花費的時間可能要遠遠大於靜下心來仔細搞懂RCU的原理要多得多。
RCU中的讀取者相對rwlock的讀取者而言,自由度更高。因為RCU的讀取者在訪問一個共享資源時,不需要考慮寫入者的陪此臘感受,這不同於rwlock的寫入者,rwlock reader在讀取共享資源時需要確保沒有寫入者在操作該資源。兩者之間的差異化源自RCU對共享資源在讀取者與寫入者之間進行了分離,而rwlock的 讀取者和寫入者則至始至終只使用共享資源的一份拷貝。這也意味著RCU中的寫入者要承擔更多的責任,而且對同一共享資源進行更新的多個寫入者之間必須引入某種互斥機制,所以RCU屬於一種"免鎖機制"的說法僅限於讀取者與寫入者之間。所以我們看到:RCU機制應該用在有大量的讀取操作,而更新操作相對較少的情形下。此時RCU可以大大提升系統系能,因為RCU的讀取操作相對其他一些有鎖機制而言,在鎖上的開銷幾乎沒有。
實際使用中,共享的資源常常以鏈表的形式存在,內核為RCU模式下的鏈表操作實現了幾個介面函數,讀取者和使用者應該使用這些內核函數,比如 list_add_tail_rcu, list_add_rcu,hlist_replace_rcu等等,具體的使用可以參考某些內核編程或者設備驅動程序方面的資料。
在釋放老指針方面,Linux內核提供兩種方法供使用者使用,一個是調用call_rcu,另一個是調用synchronize_rcu。前者是一種非同步 方式,call_rcu會將釋放老指針的回調函數放入一個結點中,然後將該結點加入到當前正在運行call_rcu的處理器的本地鏈表中,在時鍾中斷的 softirq部分(RCU_SOFTIRQ), rcu軟中斷處理函數rcu_process_callbacks會檢查當前處理器是否經歷了一個休眠期(quiescent,此處涉及內核進程調度等方面的內容),rcu的內核代碼實現在確定系統中所有的處理器都經歷過了一個休眠期之後(意味著所有處理器上都發生了一次進程切換,因此老指針此時可以被安全釋放掉了),將調用call_rcu提供的回調函數。
synchronize_rcu的實現則利用了等待隊列,在它的實現過程中也會向call_rcu那樣向當前處理器的本地鏈表中加入一個結點,與 call_rcu不同之處在於該結點中的回調函數是wakeme_after_rcu,然後synchronize_rcu將在一個等待隊列中睡眠,直到系統中所有處理器都發生了一次進程切換,因而wakeme_after_rcu被rcu_process_callbacks所調用以喚醒睡眠的 synchronize_rcu,被喚醒之後,synchronize_rcu知道它現在可以釋放老指針了。
所以我們看到,call_rcu返回後其注冊的回調函數可能還沒被調用,因而也就意味著老指針還未被釋放,而synchronize_rcu返回後老指針肯定被釋放了。所以,是調用call_rcu還是synchronize_rcu,要視特定需求與當前上下文而定,比如中斷處理的上下文肯定不能使用 synchronize_rcu函數了。 ;
5. Linux進程的調度
上回書說到 Linux進程的由來 和 Linux進程的創建 ,其實在同一時刻只能支持有限個進程或線程同時運行(這取決於CPU核數量,基本上一個進程對應一個CPU),在一個運行的操作系統上可能運行著很多進程,如果運行的進程占據CPU的時間很長,就有可能導致其他進程餓死。為了解決這種問題,操作系統引入了 進程調度器 來進行進程的切換,輪流讓各個進程使用CPU資源。
1)rq: 進程的運行隊列( runqueue), 每個CPU對應一個 ,包含自旋鎖(spinlock)、進程數量、用於公平調度的CFS信息結構、當前運行的進程描述符等。實際的進程隊列用紅黑樹來維護(通過CFS信息結構來訪問)。
2)cfs_rq: cfs調度的進程運行隊列信息 ,包含紅黑樹的根結點、正在運行的進程指針、用於負載均衡的葉子隊列等。
3)sched_entity: 把需要調度的東西抽象成調度實體 ,調度實體可以是進程、進程組、用戶等。這里包含負載權重值、對應紅黑樹結點、 虛擬運行時vruntime 等。
4)sched_class:把 調度策略(演算法)抽象成調度類 ,包含一組通用的調度操作介面。介面和實現是分離,可以根據調度介面去實現不同的調度演算法,使一個Linux調度程序可以有多個不同的調度策略。
1) 關閉內核搶占 ,初始化部分變數。獲取當前CPU的ID號,並賦值給局部變數CPU, 使rq指向CPU對應的運行隊列 。 標識當前CPU發生任務切換 ,通知RCU更新狀態,如果當前CPU處於rcu_read_lock狀態,當前進程將會放入rnp-> blkd_tasks阻塞隊列,並呈現在rnp-> gp_tasks鏈表中。 關閉本地中斷 ,獲取所要保護的運行隊列的自旋鎖, 為查找可運行進程做准備 。
2) 檢查prev的狀態,更新運行隊列 。如果不是可運行狀態,而且在內核態沒被搶占,應該從運行隊列中 刪除prev進程 。如果是非阻塞掛起信號,而且狀態為TASK_INTER-RUPTIBLE,就把該進程的狀態設置為TASK_RUNNING,並將它 插入到運行隊列 。
3)task_on_rq_queued(prev) 將pre進程插入到運行隊列的隊尾。
4)pick_next_task 選取將要執行的next進程。
5)context_switch(rq, prev, next)進行 進程上下文切換 。
1) 該進程分配的CPU時間片用完。
2) 該進程主動放棄CPU(例如IO操作)。
3) 某一進程搶佔CPU獲得執行機會。
Linux並沒有使用x86 CPU自帶的任務切換機制,需要通過手工的方式實現了切換。
進程創建後在內核的數據結構為task_struct , 該結構中有掩碼屬性cpus_allowed,4個核的CPU可以有4位掩碼,如果CPU開啟超線程,有一個8位掩碼,進程可以運行在掩碼位設置為1的CPU上。
Linux內核API提供了兩個系統調用 ,讓用戶可以修改和查看當前的掩碼:
1) sched_setaffinity():用來修改位掩碼。
2) sched_getaffinity():用來查看當前的位掩碼。
在下次task被喚醒時,select_task_rq_fair根據cpu_allowed里的掩碼來確定將其置於哪個CPU的運行隊列,一個進程在某一時刻只能存在於一個CPU的運行隊列里。
在Nginx中,使用了CPU親和度來完成某些場景的工作:
worker_processes 4;
worker_cpu_affinity 0001001001001000;
上面這個配置說明了4個工作進程中的每一個和一個CPU核掛鉤。如果這個內容寫入Nginx的配置文件中,然後Nginx啟動或者重新載入配置的時候,若worker_process是4,就會啟用4個worker,然後把worker_cpu_affinity後面的4個值當作4個cpu affinity mask,分別調用ngx_setaffinity,然後就把4個worker進程分別綁定到CPU0~3上。
worker_processes 2;
worker_cpu_affinity 01011010;
上面這個配置則說明了兩個工作進程中的每一個和2個核掛鉤。
6. 面試官:什麼是軟中斷
先來看看什麼是中斷?在計算機中,中斷是系統用來響應硬體設備請求的一種機制,操作系統收到硬體的中斷請求,會打斷正在執行的進程,然後調用內核中的中斷處理程序來響應請求。
這樣的解釋可能過於學術了,容易雲里霧里,我就舉個生活中取外賣的例子。
小林中午搬完磚,肚子餓了,點了份白切雞外賣,這次我帶閃了,沒有被某團大數據大熟。雖然平台上會顯示配送進度,但是我也不能一直傻傻地盯著呀,時間很寶貴,當然得去干別的事情,等外賣到了配送員會通過「電話」通知我,電話響了,我就會停下手中地事情,去拿外賣。
這里的打電話,其實就是對應計算機里的中斷,沒接到電話的時候,我可以做其他的事情,只有接到了電話,也就是發生中斷,我才會停下當前的事情,去進行另一個事情,也就是拿外賣。
從這個例子,我們可以知道,中斷是一種非同步的事件處理機制,可以提高系統的並發處理能力。
操作系統收到了中斷請求,會打斷其他進程的運行,所以 中斷請求的響應程序,也就是中斷處理程序,要盡可能快的執行完,這樣可以減少對正常進程運行調度地影響。
而且,中斷處理程序在響應中斷時,可能還會「臨時關閉中斷」,這意味著,如果當前中斷處理程序沒有執行完之前宏敗,系統中其他的中斷請求都無法被響應,也就說中斷有可能會丟失,所以中斷處理程序要短且快。
還是回到外賣的例子,小林到了晚上又點起了外賣,這次為了犒勞自己,共點了兩份外賣,一份小龍蝦和一份奶茶,並且是由不同地配送員來配送,那麼問題來了,當第一份外賣送到時,配送員給我打了長長的電話,說了一些雜七雜八的事情,比如給個好評等等,但如果這時另一位配送員也想給我打電話。
很明顯,這時第二位配送員因為我在通話中(相當於關閉了中斷響應),自然就無法打通我的電話,他可能嘗試了幾次後就走掉了(相當於丟失了一次中斷)。
前面虧櫻我們也提到了,中斷請求的處理程序應該要短且快,這樣才能減少對正常進程運行調度地影響,而且中斷處理程序可能會暫時關閉中斷,這時如果中斷處理程序執行時間過長,可能在還未執行完中斷處理程序前,會丟失當前其他設備的中斷請求。
那 Linux 系統 為了解決中斷處理程序執行過長和中斷丟失的問題,將中斷過程分成了兩個階段,分別是「上半部和下半部分」 。
前面的外賣例子,由於第一個配送員長時間跟我通話,則導致第二位配送員無法撥通我的電話,其實當我接到第一位配送員的電話,可以告訴配送員說我現在下樓,剩下的事情,等我們見面再說(上半部),然後就可以掛斷電話,到樓下後,在拿外賣,以及跟配送員說其他的事情(下半部)。
這樣,第一位配送員就不會佔用我手機太多時間,當第二位配送員正好過來時,會有很大幾率撥通我的電話。
再舉一個計算機中的例子,常見的網卡接收網路包的蔽空顫例子。
網卡收到網路包後,會通過 硬體中斷 通知內核有新的數據到了,於是內核就會調用對應的中斷處理程序來響應該事件,這個事件的處理也是會分成上半部和下半部。
上部分要做到快速處理,所以只要把網卡的數據讀到內存中,然後更新一下硬體寄存器的狀態,比如把狀態更新為表示數據已經讀到內存中的狀態值。
接著,內核會觸發一個 軟中斷 ,把一些處理比較耗時且復雜的事情,交給「軟中斷處理程序」去做,也就是中斷的下半部,其主要是需要從內存中找到網路數據,再按照網路協議棧,對網路數據進行逐層解析和處理,最後把數據送給應用程序。
所以,中斷處理程序的上部分和下半部可以理解為:
還有一個區別,硬中斷(上半部)是會打斷 CPU 正在執行的任務,然後立即執行中斷處理程序,而軟中斷(下半部)是以內核線程的方式執行,並且每一個 CPU 都對應一個軟中斷內核線程,名字通常為「ksoftirqd/CPU 編號」,比如 0 號 CPU 對應的軟中斷內核線程的名字是 ksoftirqd/0
不過,軟中斷不只是包括硬體設備中斷處理程序的下半部,一些內核自定義事件也屬於軟中斷,比如內核調度等、RCU 鎖(內核里常用的一種鎖)等。
在 Linux 系統里,我們可以通過查看 /proc/softirqs 的 內容來知曉「軟中斷」的運行情況,以及 /proc/interrupts 的 內容來知曉「硬中斷」的運行情況。
接下來,就來簡單的解析下 /proc/softirqs 文件的內容,在我伺服器上查看到的文件內容如下:
你可以看到,每一個 CPU 都有自己對應的不同類型軟中斷的 累計運行次數 ,有 3 點需要注意下。
第一點,要注意第一列的內容,它是代表著軟中斷的類型,在我的系統里,軟中斷包括了 10 個類型,分別對應不同的工作類型,比如 NET_RX 表示網路接收中斷,NET_TX 表示網路發送中斷、TIMER 表示定時中斷、RCU 表示 RCU 鎖中斷、SCHED 表示內核調度中斷。
第二點,要注意同一種類型的軟中斷在不同 CPU 的分布情況,正常情況下,同一種中斷在不同 CPU 上的累計次數相差不多,比如我的系統里,NET_RX 在 CPU0 、CPU1、CPU2、CPU3 上的中斷次數基本是同一個數量級,相差不多。
第三點,這些數值是系統運行以來的累計中斷次數,數值的大小沒什麼參考意義,但是系統的 中斷次數的變化速率 才是我們要關注的,我們可以使用 watch -d cat /proc/softirqs 命令查看中斷次數的變化速率。
前面提到過,軟中斷是以內核線程的方式執行的,我們可以用 ps 命令可以查看到,下面這個就是在我的伺服器上查到軟中斷內核線程的結果:
可以發現,內核線程的名字外面都有有中括弧,這說明 ps 無法獲取它們的命令行參數,所以一般來說,名字在中括弧里到,都可以認為是內核線程。
而且,你可以看到有 4 個 ksoftirqd 內核線程,這是因為我這台伺服器的 CPU 是 4 核心的,每個 CPU 核心都對應著一個內核線程。
要想知道當前的系統的軟中斷情況,我們可以使用 top 命令查看,下面是一台伺服器上的 top 的數據:
上圖中的黃色部分 si,就是 CPU 在軟中斷上的使用率,而且可以發現,每個 CPU 使用率都不高,兩個 CPU 的使用率雖然只有 3% 和 4% 左右,但是都是用在軟中斷上了。
另外,也可以看到 CPU 使用率最高的進程也是軟中斷 ksoftirqd,因此可以認為此時系統的開銷主要來源於軟中斷。
如果要知道是哪種軟中斷類型導致的,我們可以使用 watch -d cat /proc/softirqs 命令查看每個軟中斷類型的中斷次數的變化速率。
一般對於網路 I/O 比較高的 Web 伺服器,NET_RX 網路接收中斷的變化速率相比其他中斷類型快很多。
如果發現 NET_RX 網路接收中斷次數的變化速率過快,接下里就可以使用 sar -n DEV 查看網卡的網路包接收速率情況,然後分析是哪個網卡有大量的網路包進來。
接著,在通過 tcpmp 抓包,分析這些包的來源,如果是非法的地址,可以考慮加防火牆,如果是正常流量,則要考慮硬體升級等。
為了避免由於中斷處理程序執行時間過長,而影響正常進程的調度,Linux 將中斷處理程序分為上半部和下半部:
Linux 中的軟中斷包括網路收發、定時、調度、RCU 鎖等各種類型,可以通過查看 /proc/softirqs 來觀察軟中斷的累計中斷次數情況,如果要實時查看中斷次數的變化率,可以使用 watch -d cat /proc/softirqs 命令。
每一個 CPU 都有各自的軟中斷內核線程,我們還可以用 ps 命令來查看內核線程,一般名字在中括弧裡面到,都認為是內核線程。
如果在 top 命令發現,CPU 在軟中斷上的使用率比較高,而且 CPU 使用率最高的進程也是軟中斷 ksoftirqd 的時候,這種一般可以認為系統的開銷被軟中斷占據了。
這時我們就可以分析是哪種軟中斷類型導致的,一般來說都是因為網路接收軟中斷導致的,如果是的話,可以用 sar 命令查看是哪個網卡的有大量的網路包接收,再用 tcpmp 抓網路包,做進一步分析該網路包的源頭是不是非法地址,如果是就需要考慮防火牆增加規則,如果不是,則考慮硬體升級等。
7. Linux里 rcu_bh進程是做什麼的
rcu_bh有靜止狀態集合。
RCU-bh的靜止狀態是在開中斷狀態下,退出軟中斷。
需要注意的是,rcu的靜止狀態也是rcu_bh的靜止狀態。rcu的靜止狀態通過調用芹笑洞rcu_qsctr_inc()來記錄。而rcu_bh的靜嫌枯止狀態通過調用rcu_bh_qsctr_inc()來記錄。這兩個函數將它們的狀態記錄到當前CPU的rcu_data 結構中。更多知識可以參考《Linux就該升舉這么學》,裡面信息很詳細。
8. linux rcu原理
RCU, Read-Copy-Update,是Linux內核中的一種同步機制。RCU常被描述為讀寫鎖的替代品,它的特點是讀者並不需要直接與寫者進行同步,讀者與寫者也能並發的執行。
來一張圖片來描述下大體的操作吧羨薯:
多個讀者可以並發訪問臨界資源,同時使用rcu_read_lock/rcu_read_unlock來標定臨界區;
寫者(updater)在更新臨界資源的時候,拷貝一份副本作為基礎進行修改,當所有讀者離開臨界區後,把指向舊臨界資源的指針指向更新後的副本,並對舊資源進行回收處理;
圖中只顯兄羨者示一個寫者,當存在多個寫者的時候,需要在寫者之派模間進行互斥處理。
9. 編寫程序,建立一個帶有節點的單向鏈表,輸入字元串,並按從小到大順序組織到鏈表中
int main()
{
Link head; //鏈表(不帶頭節點)
int n;
printf("輸入鏈表的長度n: ");
scanf("%d",&n);
printf("連續輸入%d個數據(以空格隔開): ",n);
head=CreateLink(n);
printf(" 原本鏈表的節點是: ");
DispLink(head);
LinkSort(head);
printf(" 從大到小排序之後: ");
DispLink(head);
printf("
");
return 0;
}
鏈表的具體存儲表示為:
① 用一組任意的存儲單元來存放線性表的結點(這組存儲單元既可以是連續的,也可以是不連續的)
② 鏈表中結點的邏輯次序和物理次序不一定相同。為了能正確表示結點間的邏輯關系,在存儲每個結點值的同時,還必須存儲指示其後繼結點的地址(或位置)信息(稱為指針(pointer)或鏈(link))
鏈式存儲是最常用的存儲方式之一,它不僅可用來表示線性表,而且可用來表示各種非線性的數據結構。
以上內容參考:網路-單鏈表