Linux上有哪些操作是原子操作?

學習APUE過程中對原子操作概念有一點不清楚,請問:

(1)是否只有把指令轉換成彙編語言後,只有一條彙編指令的才是原子操作?

(2)類似read,write這樣的系統調用是不是原子操作?

(3)複合的語句呢?比如printf("readlen is %d
",read(fd,buff,buffsize))這樣的語句是不是原子操作?


Atomic 是一個「可見性」概念。當我們稱一個操作是 atomic,實際隱含了一個「對什麼 atomic」的上下文。比如說一個線程中被 mutex 保護的區域,對另一個線程是 atomic,但是對 OS kernel 本身來說並不是 atomic。

針對你的具體問題:

(1)是否只有把指令轉換成彙編語言後,只有一條彙編指令的才是原子操作?

「只有一條彙編指令」既不是 atmoic 的充分也非必要條件。首先,你忽略了「可見性」。在 thread level 上,有 mutex 就可以成為原子操作。在 thread 之下,一般來說符合 multi-processor coordinate protocol 的彙編才是原子操作。在 x86 上是一些特定的彙編加 bus lock 修飾。

(2)類似read,write這樣的系統調用是不是原子操作?

系統調用是非常複雜的情況。沒有統一的標準來規定所有系統調用的 atomicity。你可以參考

c - What happens if a write system call is called on same file by 2 different processes simultaneously

總之,假定系統調用是 atomic 是非常不切實際的編碼手段。

(3)複合的語句呢?比如printf("readlen is %d
",read(fd,buff,buffsize))這樣的語句時不時原子操作?

取決於可見性要求和 mutex 保護。


如果涉及內存訪問,就算只有一條彙編指令的在多核系統上也不一定是原子操作。


1. 在單核處理器中,一條指令能完成的操作是原子的(所以題主所說的read.write.printf對應N多彙編指令自然不是原子操作),因為中斷時發生指令之間的.

2. 在SMP(多核)結構中就不同了,因為系統有多個處理器獨立運行,即使能在單條指令中完成的操作也有可能受到干擾。所以需要lock指令,鎖住匯流排避免被干擾,實現原子操作

3. 另外可以看看內核鎖的實現.其關鍵基本都是lock實現原子操作.舉個Linux2.4內核的spin_lock例子

static inline void spin_lock(spinlock_t *lock)
{
#if SPINLOCK_DEBUG
__label__ here;
here:
if (lock-&>magic != SPINLOCK_MAGIC) {
printk("eip: %p
", here);
BUG();
}
#endif
__asm__ __volatile__(
spin_lock_string
:"=m" (lock-&>lock) : : "memory");
}

spin_lock_string是一個宏,最關鍵的lock ; decb %0
" 是一條原子操作

#define spin_lock_string
"
1: "
"lock ; decb %0
" //將lock-&>lock減1,指令帶lock表示把匯流排鎖住,禁止其他cpu訪問
"js 2f
" //不是非負數,直接進入臨界區,不用自旋
".section .text.lock,"ax"
"
"2: "
"cmpb $0,%0
" //如果-1結果為負數,那就循環檢測
"rep;nop
"
"jle 2b
" //如果檢測依舊是負數,跳轉到2
"jmp 1b
" //否則跳到標號1,檢查能否進入臨界區了
".previous"


你說的這些肯定都不是原子的,要麼用鎖,信號量,要麼用c加加atomic庫


原子操作關鍵在於不讓其他因素打擾到指令的執行,並不是指令相對較短就是原子操作。因為會有中斷或其他的機制等能破壞掉指令的順序執行。操作系統本身會用到很多原子操作(比如進程管理和控制的大部分指令,消息的發送和接受等),也提供了某些包含原子操作的系統調用(庫函數提供的鎖和信號量最終也是系統的介面)。如果能自己控制中斷,才能保證自己寫的代碼執行的時候是原子操作。鎖和信號量就是解決無法保證你的代碼是每句都是原子操作的情況下的同步問題。


其實原子操作定義本身就不清晰額。如果你用atomic_*,其實本質上你是調用了lock指令鎖匯流排,然後再串列的執行一堆指令。要說原子,cmpxchg這種硬體限制才是真正意義的原子。但是,從你使用的角度看,很多都可以是原子的。只要用各種奇淫巧計讓執行不被中斷就符合原子的定義。自旋也好,鎖匯流排也好,互斥也好。


不要假定任何一種看起來執行時間不長的操作是原子的。要在編程層面追求原子,最安全的辦法就是使用內核提供的現成的原子操作或者同步操作介面。


(1)是否只有把指令轉換成彙編語言後,只有一條彙編指令的才是原子操作?

一條彙編確實能保證原子性,我們看cpu執行指令的過程,取指,執行,如果加上間接定址,就在取指和執行之間加上一個間指周期,如果有中斷,就在執行周期之後加上一個中斷周期。

=====&>指令周期:取指、間指、執行、中斷

中斷(跳轉到其他程序)發生在指令執行完成之後。

(2)、類似read,write這樣的系統調用是不是原子操作?

ps:第一次見到原子性的概念好像是在計算機組成原理這本書中,講到中斷這一塊

來看下中斷的處理過程。

關中斷------&>保護現場-------&>調到中斷執行程序執行------&>恢復現場------&>開中斷。

(假設是單核)

因為關中斷的原因,導致cpu不再響應中斷(這裡是有點問題,中斷比較複雜,從分類上看,中斷可以分為軟中斷和硬中斷,其中硬體中斷中有分為外部中斷和內部中斷,內部中斷時不可屏蔽的中斷,包括硬體出錯或者運行錯誤,這類中斷時不可屏蔽的,不可屏蔽的意思是其實關中斷,依然能夠得到響應,還有一部分是關於中斷重入的,這個也比較複雜,處理起來很麻煩,但是就當前這個觀點看,不影響我們這裡說的這個部分),也就防止其他中斷服務程序亂入,從而保證當前中斷服務程序的原子性。直到開中斷為止。

但是

(3)、複合的語句呢?比如printf("readlen is %d
",read(fd,buff,buffsize))這樣的語句是不是原子操作?

printf並不是原子操作的,如果你看過《一個操作系統的實現》的話,可以知道printf分為幾個部分,首先是將參數中傳入的字元串結合參數整理一個buffer中,然後調用write()這個系統調用,來完成列印的工作。

即在整理字元串的過程中,是可以被打斷的。而且上面也說write()是可以被打斷的。

下面是一些擴展:

我第二次遇到原子性的概念是在計算機操作系統關於進程同步的內容的時候,進程之間共享資源,

我用生產者和消費者這問題來描:

生產者和消費者共享N個緩衝塊。

producer:

while(true){

while(count==N){

}

buffer[in]=nextp;

in=(in+1)%N;

count++;

}

consumer:

while(true){

while(count==0){

}

nextc=buffer[out];

out=(out+1)%N;

count--;

}

上述是生產者進程和消費者進程的偽代碼。

假設現在調度到生產者進程,並且count=0。

生產者執行到:in=(in+1)%N;這條語句,然後cpu相應時鐘中斷,調度到消費者進程,

這個時候緩衝區已經有一個產品了,但是因為上一次調度時,生產者沒有執行count++,導致消費者進程檢測到的count=0,使得cpu在原地空轉。

是不是覺得也沒有多大問題啊,最多就是多了一個cpu調度,降低了cpu的執行效率嘛。

如果你這樣覺得,那我再來舉一個生產者和消費者問題的例子。

我們把上面關於生產者和消費者的偽代碼擴展一下,加入阻塞(sleep()wakeup())的概念

producer:

while(ture){

if(count==N){

sleep();

}

buffer[in]=nextp;

in=(in+1)%N;

count++;

if(count==1){

wakeup(consumer);

}

}

consumer:

while(ture){

if(count==0){

sleep();

}

buffer[in]=nextp;

in=(in+1)%N;

count++;

if(count==N-1){

wakeup(producer);

}

}

現在再來看下 這段代碼會出現的問題。

假設現在調度到消費者進程,並且count=0。

消費者進程檢測到count==0,這個時候cpu響應時鐘中斷,從而調度到生產者進程,生產者進程在緩衝區放入一個產品,count++之後以為count=1,判斷消費者進程阻塞,就發送一個wakeup(其實就是找到消費者進程的pcb,修改消費者pcb中的進程狀態)信號給消費者,然後時鐘中斷再次調度到消費者,完成上下文切換,這個時候要注意,消費者本身是清醒的,雖然檢測到count=0,但是還沒來得及sleep()自己,就調度到下一個進程了,所以完成上下文切換之後,消費者進程會先sleep()自己。

生產者進程不知道消費者沉睡,生產產品之後,只要緩衝區為空,就將產品放進去,但是因為消費者進程沉睡,緩衝區總有滿的時候,那麼,生產者進程也陷入沉睡。

這個結果就是消費者進程和生產者進程發生死鎖。

=====》分析上面兩個生產者和消費者問題,都是因為臨界區的代碼沒有執行完,發生調度,兩外一個進程也進入自己的臨界區操作共享資源(上述問題的中共享資源是count)造成的。

想要解決這個問題,就要保證進程對共享資源的操作(要麼就都執行,要麼不執行)就是讓進程對共享資源互斥的進行訪問。也就是說,進程在操作共享資源的過程不能被打斷。

=-=======》具體的解決方法就不說了,書上有非常多,更好的解決方法,比如信號量,互斥鎖(信號量的一般方法),自旋鎖等等

ps:要注意信號量本身也是被多個進程(教材上寫的是進程,但是有些人說是線程)共享的,所以對信號量的操作也要保證原子性。

上面說的是單核的,那麼如果是多核的環境中,單純的屏蔽中斷,就不再是一個合適的方式了。

比如屏蔽了這個cpu的中斷,其他的cpu運行的進程依然可以執行內存讀寫的操作。

解決這個問題的最簡單的方式,就是鎖住內存匯流排。

一般硬體提供的了兩條指令 一條指令叫做 TSl(這條指令的作用叫做測試並上鎖)

還有一條指令,xchang(好像叫這個)實現的思想其實是和TSL一樣的


同樓上,存在資源競爭的情況下還是需要加鎖 。


推薦閱讀:

怎樣評價《無根的根:無名師的 Unix 心傳》?
Linux 上有哪些工具軟體堪稱精美?
有哪些在線 Linux 環境可以 ssh 登錄來玩?
監控程序如何編寫單元測試?
王垠當年提倡「完全用 linux 工作」,然而最近又發文挺 windows 噴 unix,這中間究竟經歷了什麼?

TAG:Linux | Unix | Linux開發 | Linux內核 | UNIX環境高級編程書籍 |