當前位置:首頁 » 編程軟體 » linux多線程服務端編程

linux多線程服務端編程

發布時間: 2022-10-03 08:04:25

㈠ 服務端通過UDP接收的數據能否多線程處理

多線程並不能提高執行效率,相反,由於多了線程切換,會有一丁點額外開銷
多線程能提高交互和響應效率。
原因是cpu只能一條一條指令的做。
如果是多核編程或多處理器編程,那就肯定能快了。

㈡ 想接觸C++多線程編程,需要從哪方面入手,有沒有

多線程編程的難點不在於鎖,正常人看一下操作系統再寫幾個線程demo就可以基本理解了。對於C++而言,甚至連編寫線程安全的類也不是難事。只需要用同步原語來保持對共享資源的訪問即可。
我個人覺得最需要的就是實戰,寫Demo誰都會寫。同步原語就那麼幾個,信號量,互斥量,條件變數等。但是怎麼用呢?當你從點擊星際爭霸到和玩家匹配進行游戲,這當中程序是怎麼運行的?
事件驅動是怎麼驅動的?
就目前來說,我遇到的困難不是線程的死鎖,而是對並發模型的理解。Actor,Reactor模式等。這些東西不實戰,個人空想理解起來會吃力。
推薦《linux多線程服務端編程》,這本書給我的觀點是實戰性很強,而且涉及面也比較廣。後幾章提到了分布式系統和作者對C++的思考以及STL algotrithm的運用。如作者所說:「對於面向對象,封裝式必須的;但繼承和多態耦合性太強,很不劃算」我就很贊同

同時展示了一個用C++開發的網路庫,不過雖然看了這本書,我還是沒找到為什麼要用C++的理由。我認為C的確可以很好地解決問題。C++的話就RAII算是真的有益處。
但讀之前你需要有一定的C++和操作系統基礎。當時買這本書的時候還覺得有點心疼,現在看看物超所值。(我那本CSAPP就翻了一章=-=)
總結:看現代操作系統第二章,同時結合C++11的thread庫寫經典Demo(生產者消費者問題等)
花兩周左右。剩下的就是實戰。如果不實戰,你還是不知道這些東西在生產環境中是怎麼使用的。
可以結合muo skynet等開源網路框架學習並發模型。

㈢ Unix C多線程編程 線程間變數覆蓋問題

變數不會自己被覆蓋,這種情況多數是內存溢出導致污染了其他線程棧

第二個問題,貼上我以前對這個問題的回答:

C99標准之前是不可以這么聲明數組的,但是C99開始引入了變長數組這一概念,也就是使用變數定義數組各維,也就是你可以用 int a[x]; 這種方式定義數組,x的值無需是常量,但是有幾個主要限制:
1:必須在函數內部聲明或者是函數參數(也就是必須在存儲在棧區),也不能是成員變數
2:不能在聲明的同時初始化
3:不能是靜態變數或用extern修飾
4:數組的類型以及長度的類型都必須支持sizeof(一般來說就是只能用內部類型)

大部分支持C99的編譯器都支持這個特性(VC2005之後,GCC3.2之後),這個和new出來的數組還是本質上不一樣的,這個其實是程序在運行期間在進程棧區生成定長數組,所以也不需要你手動釋放,但這種做法對一些高級的調試方法可能有一定的影響

㈣ 如何看懂《Linux多線程服務端編程

一:進程和線程
每個進程有自己獨立的地址空間。「在同一個進程」還是「不在同一個進程」是系統功能劃分的重要決策點。《Erlang程序設計》[ERL]把進程比喻為人:
每個人有自己的記憶(內存),人與人通過談話(消息傳遞)來交流,談話既可以是面談(同一台伺服器),也可以在電話里談(不同的伺服器,有網路通信)。面談和電話談的區別在於,面談可以立即知道對方是否死了(crash,SIGCHLD),而電話談只能通過周期性的心跳來判斷對方是否還活著。
有了這些比喻,設計分布式系統時可以採取「角色扮演」,團隊里的幾個人各自扮演一個進程,人的角色由進程的代碼決定(管登錄的、管消息分發的、管買賣的等等)。每個人有自己的記憶,但不知道別人的記憶,要想知道別人的看法,只能通過交談(暫不考慮共享內存這種IPC)。然後就可以思考:
·容錯:萬一有人突然死了
·擴容:新人中途加進來
·負載均衡:把甲的活兒挪給乙做
·退休:甲要修復bug,先別派新任務,等他做完手上的事情就把他重啟
等等各種場景,十分便利。

線程的特點是共享地址空間,從而可以高效地共享數據。一台機器上的多個進程能高效地共享代碼段(操作系統可以映射為同樣的物理內存),但不能共享數據。如果多個進程大量共享內存,等於是把多進程程序當成多線程來寫,掩耳盜鈴。
「多線程」的價值,我認為是為了更好地發揮多核處理器(multi-cores)的效能。在單核時代,多線程沒有多大價值(個人想法:如果要完成的任務是CPU密集型的,那多線程沒有優勢,甚至因為線程切換的開銷,多線程反而更慢;如果要完成的任務既有CPU計算,又有磁碟或網路IO,則使用多線程的好處是,當某個線程因為IO而阻塞時,OS可以調度其他線程執行,雖然效率確實要比任務的順序執行效率要高,然而,這種類型的任務,可以通過單線程的」non-blocking IO+IO multiplexing」的模型(事件驅動)來提高效率,採用多線程的方式,帶來的可能僅僅是編程上的簡單而已)。Alan Cox說過:」A computer is a state machine.Threads are for people who can』t program state machines.」(計算機是一台狀態機。線程是給那些不能編寫狀態機程序的人准備的)如果只有一塊CPU、一個執行單元,那麼確實如Alan Cox所說,按狀態機的思路去寫程序是最高效的。

二:單線程伺服器的常用編程模型
據我了解,在高性能的網路程序中,使用得最為廣泛的恐怕要數」non-blocking IO + IO multiplexing」這種模型,即Reactor模式。
在」non-blocking IO + IO multiplexing」這種模型中,程序的基本結構是一個事件循環(event loop),以事件驅動(event-driven)和事件回調的方式實現業務邏輯:
[cpp] view plain
//代碼僅為示意,沒有完整考慮各種情況
while(!done)
{
int timeout_ms = max(1000, getNextTimedCallback());
int retval = poll(fds, nfds, timeout_ms);
if (retval<0){
處理錯誤,回調用戶的error handler
}else{
處理到期的timers,回調用戶的timer handler
if(retval>0){
處理IO事件,回調用戶的IO event handler
}
}
}

這里select(2)/poll(2)有伸縮性方面的不足(描述符過多時,效率較低),Linux下可替換為epoll(4),其他操作系統也有對應的高性能替代品。
Reactor模型的優點很明顯,編程不難,效率也不錯。不僅可以用於讀寫socket,連接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式進行,以提高並發度和吞吐量(throughput),對於IO密集的應用是個不錯的選擇。lighttpd就是這樣,它內部的fdevent結構十分精妙,值得學習。
基於事件驅動的編程模型也有其本質的缺點,它要求事件回調函數必須是非阻塞的。對於涉及網路IO的請求響應式協議,它容易割裂業務邏輯,使其散布於多個回調函數之中,相對不容易理解和維護。

三:多線程伺服器的常用編程模型
大概有這么幾種:
a:每個請求創建一個線程,使用阻塞式IO操作。在Java 1.4引人NIO之前,這是Java網路編程的推薦做法。可惜伸縮性不佳(請求太多時,操作系統創建不了這許多線程)。
b:使用線程池,同樣使用阻塞式IO操作。與第1種相比,這是提高性能的措施。
c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。
d:Leader/Follower等高級模式。
在默認情況下,我會使用第3種,即non-blocking IO + one loop per thread模式來編寫多線程C++網路服務程序。

1:one loop per thread
此種模型下,程序里的每個IO線程有一個event loop,用於處理讀寫和定時事件(無論周期性的還是單次的)。代碼框架跟「單線程伺服器的常用編程模型」一節中的一樣。
libev的作者說:
One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.

這種方式的好處是:
a:線程數目基本固定,可以在程序啟動的時候設置,不會頻繁創建與銷毀。
b:可以很方便地在線程間調配負載。
c:IO事件發生的線程是固定的,同一個TCP連接不必考慮事件並發。

Event loop代表了線程的主循環,需要讓哪個線程幹活,就把timer或IO channel(如TCP連接)注冊到哪個線程的loop里即可:對實時性有要求的connection可以單獨用一個線程;數據量大的connection可以獨佔一個線程,並把數據處理任務分攤到另幾個計算線程中(用線程池);其他次要的輔助性connections可以共享一個線程。
比如,在dbproxy中,一個線程用於專門處理客戶端發來的管理命令;一個線程用於處理客戶端發來的MySQL命令,而與後端資料庫通信執行該命令時,是將該任務分配給所有事件線程處理的。

對於non-trivial(有一定規模)的服務端程序,一般會採用non-blocking IO + IO multiplexing,每個connection/acceptor都會注冊到某個event loop上,程序里有多個event loop,每個線程至多有一個event loop。
多線程程序對event loop提出了更高的要求,那就是「線程安全」。要允許一個線程往別的線程的loop里塞東西,這個loop必須得是線程安全的。
在dbproxy中,線程向其他線程分發任務,是通過管道和隊列實現的。比如主線程accept到連接後,將表示該連接的結構放入隊列,並向管道中寫入一個位元組。計算線程在自己的event loop中注冊管道的讀事件,一旦有數據可讀,就嘗試從隊列中取任務。

2:線程池
不過,對於沒有IO而光有計算任務的線程,使用event loop有點浪費。可以使用一種補充方案,即用blocking queue實現的任務隊列:
[cpp] view plain
typedef boost::function<void()>Functor;
BlockingQueue<Functor> taskQueue; //線程安全的全局阻塞隊列

//計算線程
void workerThread()
{
while (running) //running變數是個全局標志
{
Functor task = taskQueue.take(); //this blocks
task(); //在產品代碼中需要考慮異常處理
}
}

// 創建容量(並發數)為N的線程池
int N = num_of_computing_threads;
for (int i = 0; i < N; ++i)
{
create_thread(&workerThread); //啟動線程
}

//向任務隊列中追加任務
Foo foo; //Foo有calc()成員函數
boost::function<void()> task = boost::bind(&Foo::calc,&foo);
taskQueue.post(task);

除了任務隊列,還可以用BlockingQueue<T>實現數據的生產者消費者隊列,即T是數據類型而非函數對象,queue的消費者從中拿到數據進行處理。其實本質上是一樣的。

3:總結
總結而言,我推薦的C++多線程服務端編程模式為:one (event) loop per thread + thread pool:
event loop用作IO multiplexing,配合non-blockingIO和定時器;
thread pool用來做計算,具體可以是任務隊列或生產者消費者隊列。

以這種方式寫伺服器程序,需要一個優質的基於Reactor模式的網路庫來支撐,muo正是這樣的網路庫。比如dbproxy使用的是libevent。
程序里具體用幾個loop、線程池的大小等參數需要根據應用來設定,基本的原則是「阻抗匹配」(解釋見下),使得CPU和IO都能高效地運作。所謂阻抗匹配原則:
如果池中線程在執行任務時,密集計算所佔的時間比重為 P (0 < P <= 1),而系統一共有 C 個 CPU,為了讓這 C 個 CPU 跑滿而又不過載,線程池大小的經驗公式 T = C/P。(T 是個 hint,考慮到 P 值的估計不是很准確,T 的最佳值可以上下浮動 50%)
以後我再講這個經驗公式是怎麼來的,先驗證邊界條件的正確性。
假設 C = 8,P = 1.0,線程池的任務完全是密集計算,那麼T = 8。只要 8 個活動線程就能讓 8 個 CPU 飽和,再多也沒用,因為 CPU 資源已經耗光了。
假設 C = 8,P = 0.5,線程池的任務有一半是計算,有一半等在 IO 上,那麼T = 16。考慮操作系統能靈活合理地調度 sleeping/writing/running 線程,那麼大概 16 個「50%繁忙的線程」能讓 8 個 CPU 忙個不停。啟動更多的線程並不能提高吞吐量,反而因為增加上下文切換的開銷而降低性能。
如果 P < 0.2,這個公式就不適用了,T 可以取一個固定值,比如 5*C。

另外,公式里的 C 不一定是 CPU 總數,可以是「分配給這項任務的 CPU 數目」,比如在 8 核機器上分出 4 個核來做一項任務,那麼 C=4。

四:進程間通信只用TCP
Linux下進程間通信的方式有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息隊列、共享內存、信號(signals),以及Socket。同步原語有互斥器(mutex)、條件變數(condition variable)、讀寫鎖(reader-writer lock)、文件鎖(record locking)、信號量(semaphore)等等。

進程間通信我首選Sockets(主要指TCP,我沒有用過UDP,也不考慮Unix domain協議)。其好處在於:
可以跨主機,具有伸縮性。反正都是多進程了,如果一台機器的處理能力不夠,很自然地就能用多台機器來處理。把進程分散到同一區域網的多台機器上,程序改改host:port配置就能繼續用;
TCP sockets和pipe都是操作文件描述符,用來收發位元組流,都可以read/write/fcntl/select/poll等。不同的是,TCP是雙向的,Linux的pipe是單向的,進程間雙向通信還得開兩個文件描述符,不方便;而且進程要有父子關系才能用pipe,這些都限制了pipe的使用;
TCP port由一個進程獨占,且進程退出時操作系統會自動回收文件描述符。因此即使程序意外退出,也不會給系統留下垃圾,程序重啟之後能比較容易地恢復,而不需要重啟操作系統(用跨進程的mutex就有這個風險);而且,port是獨占的,可以防止程序重復啟動,後面那個進程搶不到port,自然就沒法初始化了,避免造成意料之外的結果;
與其他IPC相比,TCP協議的一個天生的好處是「可記錄、可重現」。tcpmp和Wireshark是解決兩個進程間協議和狀態爭端的好幫手,也是性能(吞吐量、延遲)分析的利器。我們可以藉此編寫分布式程序的自動化回歸測試。也可以用tcp之類的工具進行壓力測試。TCP還能跨語言,服務端和客戶端不必使用同一種語言。

分布式系統的軟體設計和功能劃分一般應該以「進程」為單位。從宏觀上看,一個分布式系統是由運行在多台機器上的多個進程組成的,進程之間採用TCP長連接通信。
使用TCP長連接的好處有兩點:一是容易定位分布式系統中的服務之間的依賴關系。只要在機器上運行netstat -tpna|grep <port>就能立刻列出用到某服務的客戶端地址(Foreign Address列),然後在客戶端的機器上用netstat或lsof命令找出是哪個進程發起的連接。TCP短連接和UDP則不具備這一特性。二是通過接收和發送隊列的長度也較容易定位網路或程序故障。在正常運行的時候,netstat列印的Recv-Q和Send-Q都應該接近0,或者在0附近擺動。如果Recv-Q保持不變或持續增加,則通常意味著服務進程的處理速度變慢,可能發生了死鎖或阻塞。如果Send-Q保持不變或持續增加,有可能是對方伺服器太忙、來不及處理,也有可能是網路中間某個路由器或交換機故障造成丟包,甚至對方伺服器掉線,這些因素都可能表現為數據發送不出去。通過持續監控Recv-Q和Send-Q就能及早預警性能或可用性故障。以下是服務端線程阻塞造成Recv-Q和客戶端Send-Q激增的例子:
[cpp] view plain
$netstat -tn
Proto Recv-Q Send-Q Local Address Foreign
tcp 78393 0 10.0.0.10:2000 10.0.0.10:39748 #服務端連接
tcp 0 132608 10.0.0.10:39748 10.0.0.10:2000 #客戶端連接
tcp 0 52 10.0.0.10:22 10.0.0.4:55572

五:多線程伺服器的適用場合
如果要在一台多核機器上提供一種服務或執行一個任務,可用的模式有:
a:運行一個單線程的進程;
b:運行一個多線程的進程;
c:運行多個單線程的進程;
d:運行多個多線程的進程;

考慮這樣的場景:如果使用速率為50MB/s的數據壓縮庫,進程創建銷毀的開銷是800微秒,線程創建銷毀的開銷是50微秒。如何執行壓縮任務?
如果要偶爾壓縮1GB的文本文件,預計運行時間是20s,那麼起一個進程去做是合理的,因為進程啟動和銷毀的開銷遠遠小於實際任務的耗時。
如果要經常壓縮500kB的文本數據,預計運行時間是10ms,那麼每次都起進程 似乎有點浪費了,可以每次單獨起一個線程去做。
如果要頻繁壓縮10kB的文本數據,預計運行時間是200微秒,那麼每次起線程似 乎也很浪費,不如直接在當前線程搞定。也可以用一個線程池,每次把壓縮任務交給線程池,避免阻塞當前線程(特別要避免阻塞IO線程)。
由此可見,多線程並不是萬靈丹(silver bullet)。

1:必須使用單線程的場合
據我所知,有兩種場合必須使用單線程:
a:程序可能會fork(2);
實際編程中,應該保證只有單線程程序能進行fork(2)。多線程程序不是不能調用fork(2),而是這么做會遇到很多麻煩:
fork一般不能在多線程程序中調用,因為Linux的fork只克隆當前線程的thread of control,不可隆其他線程。fork之後,除了當前線程之外,其他線程都消失了。
這就造成一種危險的局面。其他線程可能正好處於臨界區之內,持有了某個鎖,而它突然死亡,再也沒有機會去解鎖了。此時如果子進程試圖再對同一個mutex加鎖,就會立即死鎖。因此,fork之後,子進程就相當於處於signal handler之中(因為不知道調用fork時,父進程中的線程此時正在調用什麼函數,這和信號發生時的場景一樣),你不能調用線程安全的函數(除非它是可重入的),而只能調用非同步信號安全的函數。比如,fork之後,子進程不能調用:
malloc,因為malloc在訪問全局狀態時幾乎肯定會加鎖;
任何可能分配或釋放內存的函數,比如snprintf;
任何Pthreads函數;
printf系列函數,因為其他線程可能恰好持有stdout/stderr的鎖;
除了man 7 signal中明確列出的信號安全函數之外的任何函數。

因此,多線程中調用fork,唯一安全的做法是fork之後,立即調用exec執行另一個程序,徹底隔斷子進程與父進程的聯系。

在多線程環境中調用fork,產生子進程後。子進程內部只存在一個線程,也就是父進程中調用fork的線程的副本。
使用fork創建子進程時,子進程通過繼承整個地址空間的副本,也從父進程那裡繼承了所有互斥量、讀寫鎖和條件變數的狀態。如果父進程中的某個線程佔有鎖,則子進程同樣佔有這些鎖。問題是子進程並不包含佔有鎖的線程的副本,所以子進程沒有辦法知道它佔有了哪些鎖,並且需要釋放哪些鎖。
盡管Pthread提供了pthread_atfork函數試圖繞過這樣的問題,但是這回使得代碼變得混亂。因此《Programming With Posix Threads》一書的作者說:」Avoid using fork in threaded code except where the child process will immediately exec a new program.」。

b:限製程序的CPU佔用率;
這個很容易理解,比如在一個8核的伺服器上,一個單線程程序即便發生busy-wait,占滿1個core,其CPU使用率也只有12.5%,在這種最壞的情況下,系統還是有87.5%的計算資源可供其他服務進程使用。
因此對於一些輔助性的程序,如果它必須和主要服務進程運行在同一台機器的話,那麼做成單線程的能避免過分搶奪系統的計算資源。

㈤ 怎樣學習python

python作為當下最熱門的計算機編程語言之一,是許多互聯網大廠(如阿里騰訊等)在招聘時會作出要求的能力之一。學好python對於將來大數據方向、雲計算方向等物聯網時代新興崗位的學習很有幫助。

想要學習Python,需要掌握的內容還是比較多的,對於自學的同學來說會有一些難度,不推薦自學能力差的人。我們將學習的過程劃分為4個階段,每個階段學習對應的內容,具體的學習順序如下:

Python學習順序:

①Python軟體開發基礎

  • 掌握計算機的構成和工作原理

  • 會使用Linux常用工具

  • 熟練使用Docker的基本命令

  • 建立Python開發環境,並使用print輸出

  • 使用Python完成字元串的各種操作

  • 使用Python re模塊進行程序設計

  • 使用Python創建文件、訪問、刪除文件

  • 掌握import 語句、From…import 語句、From…import* 語句、方法的引用、Python中的包

  • ②Python軟體開發進階

  • 能夠使用Python面向對象方法開發軟體

  • 能夠自己建立資料庫,表,並進行基本資料庫操作

  • 掌握非關系資料庫MongoDB的使用,掌握Redis開發

  • 能夠獨立完成TCP/UDP服務端客戶端軟體開發,能夠實現ftp、http伺服器,開發郵件軟體

  • 能開發多進程、多線程軟體

  • ③Python全棧式WEB工程師

  • 能夠獨立完成後端軟體開發,深入理解Python開發後端的精髓

  • 能夠獨立完成前端軟體開發,並和後端結合,熟練掌握使用Python進行全站Web開發的技巧

  • ④Python多領域開發

  • 能夠使用Python熟練編寫爬蟲軟體

  • 能夠熟練使用Python庫進行數據分析

  • 招聘網站Python招聘職位數據爬取分析

  • 掌握使用Python開源人工智慧框架進行人工智慧軟體開發、語音識別、人臉識別

  • 掌握基本設計模式、常用演算法

  • 掌握軟體工程、項目管理、項目文檔、軟體測試調優的基本方法

想要系統學習,你可以考察對比一下開設有IT專業的熱門學校,好的學校擁有根據當下企業需求自主研發課程的能力,建議實地考察對比一下。

祝你學有所成,望採納。

㈥ C# socket伺服器端 多線程客戶端 如何少量使用CPU

1)開啟的線程設置為後台線程
Thread t = new Thread(你的線程函數);
t.IsBackground = true
2)建議你利用socket提供的非同步回調 socket.BeginXXXXX() socket.EndXXXX 來實現多線程處理
-----------
利用非同步回調最大的好處是:(1)可以充分利用.net framework 的線程池中I/O線程,最大限度降低CPU處理開銷。曾經做過處理1000多個socket TCP客戶端連接的程序,雙核CPU平均佔用不超過12%(2)網路通信過程各種異常是不可避免的,非同步編程結構很容易實現異常處理。

㈦ 關於Socket多線程編程的recv函數問題

建議設個flag,在服務端進這個while後,如果沒處理(文件沒發送)完畢前,不在進入,或多開幾個thread設置不同埠等待接收客戶端的要求。

㈧ 如何看懂《Linux多線程服務端編程

一:進程和線程
每個進程有自己獨立的地址空間。「在同一個進程」還是「不在同一個進程」是系統功能劃分的重要決策點。《Erlang程序設計》[ERL]把進程比喻為人:
每個人有自己的記憶(內存),人與人通過談話(消息傳遞)來交流,談話既可以是面談(同一台伺服器),也可以在電話里談(不同的伺服器,有網路通信)。面談和電話談的區別在於,面談可以立即知道對方是否死了(crash,SIGCHLD),而電話談只能通過周期性的心跳來判斷對方是否還活著。
有了這些比喻,設計分布式系統時可以採取「角色扮演」,團隊里的幾個人各自扮演一個進程,人的角色由進程的代碼決定(管登錄的、管消息分發的、管買賣的等等)。每個人有自己的記憶,但不知道別人的記憶,要想知道別人的看法,只能通過交談(暫不考慮共享內存這種IPC)。然後就可以思考:
·容錯:萬一有人突然死了
·擴容:新人中途加進來
·負載均衡:把甲的活兒挪給乙做
·退休:甲要修復bug,先別派新任務,等他做完手上的事情就把他重啟
等等各種場景,十分便利。

線程的特點是共享地址空間,從而可以高效地共享數據。一台機器上的多個進程能高效地共享代碼段(操作系統可以映射為同樣的物理內存),但不能共享數據。如果多個進程大量共享內存,等於是把多進程程序當成多線程來寫,掩耳盜鈴。
「多線程」的價值,我認為是為了更好地發揮多核處理器(multi-cores)的效能。在單核時代,多線程沒有多大價值(個人想法:如果要完成的任務是CPU密集型的,那多線程沒有優勢,甚至因為線程切換的開銷,多線程反而更慢;如果要完成的任務既有CPU計算,又有磁碟或網路IO,則使用多線程的好處是,當某個線程因為IO而阻塞時,OS可以調度其他線程執行,雖然效率確實要比任務的順序執行效率要高,然而,這種類型的任務,可以通過單線程的」non-blocking IO+IO multiplexing」的模型(事件驅動)來提高效率,採用多線程的方式,帶來的可能僅僅是編程上的簡單而已)。Alan Cox說過:」A computer is a state machine.Threads are for people who can』t program state machines.」(計算機是一台狀態機。線程是給那些不能編寫狀態機程序的人准備的)如果只有一塊CPU、一個執行單元,那麼確實如Alan Cox所說,按狀態機的思路去寫程序是最高效的。

二:單線程伺服器的常用編程模型
據我了解,在高性能的網路程序中,使用得最為廣泛的恐怕要數」non-blocking IO + IO multiplexing」這種模型,即Reactor模式。
在」non-blocking IO + IO multiplexing」這種模型中,程序的基本結構是一個事件循環(event loop),以事件驅動(event-driven)和事件回調的方式實現業務邏輯:
[cpp] view plain
//代碼僅為示意,沒有完整考慮各種情況
while(!done)
{
int timeout_ms = max(1000, getNextTimedCallback());
int retval = poll(fds, nfds, timeout_ms);
if (retval<0){
處理錯誤,回調用戶的error handler
}else{
處理到期的timers,回調用戶的timer handler
if(retval>0){
處理IO事件,回調用戶的IO event handler
}
}
}

這里select(2)/poll(2)有伸縮性方面的不足(描述符過多時,效率較低),Linux下可替換為epoll(4),其他操作系統也有對應的高性能替代品。
Reactor模型的優點很明顯,編程不難,效率也不錯。不僅可以用於讀寫socket,連接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式進行,以提高並發度和吞吐量(throughput),對於IO密集的應用是個不錯的選擇。lighttpd就是這樣,它內部的fdevent結構十分精妙,值得學習。
基於事件驅動的編程模型也有其本質的缺點,它要求事件回調函數必須是非阻塞的。對於涉及網路IO的請求響應式協議,它容易割裂業務邏輯,使其散布於多個回調函數之中,相對不容易理解和維護。

三:多線程伺服器的常用編程模型
大概有這么幾種:
a:每個請求創建一個線程,使用阻塞式IO操作。在Java 1.4引人NIO之前,這是Java網路編程的推薦做法。可惜伸縮性不佳(請求太多時,操作系統創建不了這許多線程)。
b:使用線程池,同樣使用阻塞式IO操作。與第1種相比,這是提高性能的措施。
c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。
d:Leader/Follower等高級模式。
在默認情況下,我會使用第3種,即non-blocking IO + one loop per thread模式來編寫多線程C++網路服務程序。

1:one loop per thread
此種模型下,程序里的每個IO線程有一個event loop,用於處理讀寫和定時事件(無論周期性的還是單次的)。代碼框架跟「單線程伺服器的常用編程模型」一節中的一樣。
libev的作者說:
One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.

這種方式的好處是:
a:線程數目基本固定,可以在程序啟動的時候設置,不會頻繁創建與銷毀。
b:可以很方便地在線程間調配負載。
c:IO事件發生的線程是固定的,同一個TCP連接不必考慮事件並發。

Event loop代表了線程的主循環,需要讓哪個線程幹活,就把timer或IO channel(如TCP連接)注冊到哪個線程的loop里即可:對實時性有要求的connection可以單獨用一個線程;數據量大的connection可以獨佔一個線程,並把數據處理任務分攤到另幾個計算線程中(用線程池);其他次要的輔助性connections可以共享一個線程。
比如,在dbproxy中,一個線程用於專門處理客戶端發來的管理命令;一個線程用於處理客戶端發來的MySQL命令,而與後端資料庫通信執行該命令時,是將該任務分配給所有事件線程處理的。

對於non-trivial(有一定規模)的服務端程序,一般會採用non-blocking IO + IO multiplexing,每個connection/acceptor都會注冊到某個event loop上,程序里有多個event loop,每個線程至多有一個event loop。
多線程程序對event loop提出了更高的要求,那就是「線程安全」。要允許一個線程往別的線程的loop里塞東西,這個loop必須得是線程安全的。
在dbproxy中,線程向其他線程分發任務,是通過管道和隊列實現的。比如主線程accept到連接後,將表示該連接的結構放入隊列,並向管道中寫入一個位元組。計算線程在自己的event loop中注冊管道的讀事件,一旦有數據可讀,就嘗試從隊列中取任務。

2:線程池
不過,對於沒有IO而光有計算任務的線程,使用event loop有點浪費。可以使用一種補充方案,即用blocking queue實現的任務隊列:
[cpp] view plain
typedef boost::function<void()>Functor;
BlockingQueue<Functor> taskQueue; //線程安全的全局阻塞隊列

//計算線程
void workerThread()
{
while (running) //running變數是個全局標志
{
Functor task = taskQueue.take(); //this blocks
task(); //在產品代碼中需要考慮異常處理
}
}

// 創建容量(並發數)為N的線程池
int N = num_of_computing_threads;
for (int i = 0; i < N; ++i)
{
create_thread(&workerThread); //啟動線程
}

//向任務隊列中追加任務
Foo foo; //Foo有calc()成員函數
boost::function<void()> task = boost::bind(&Foo::calc,&foo);
taskQueue.post(task);

除了任務隊列,還可以用BlockingQueue<T>實現數據的生產者消費者隊列,即T是數據類型而非函數對象,queue的消費者從中拿到數據進行處理。其實本質上是一樣的。

3:總結
總結而言,我推薦的C++多線程服務端編程模式為:one (event) loop per thread + thread pool:
event loop用作IO multiplexing,配合non-blockingIO和定時器;
thread pool用來做計算,具體可以是任務隊列或生產者消費者隊列。

以這種方式寫伺服器程序,需要一個優質的基於Reactor模式的網路庫來支撐,muo正是這樣的網路庫。比如dbproxy使用的是libevent。
程序里具體用幾個loop、線程池的大小等參數需要根據應用來設定,基本的原則是「阻抗匹配」(解釋見下),使得CPU和IO都能高效地運作。所謂阻抗匹配原則:
如果池中線程在執行任務時,密集計算所佔的時間比重為 P (0 < P <= 1),而系統一共有 C 個 CPU,為了讓這 C 個 CPU 跑滿而又不過載,線程池大小的經驗公式 T = C/P。(T 是個 hint,考慮到 P 值的估計不是很准確,T 的最佳值可以上下浮動 50%)
以後我再講這個經驗公式是怎麼來的,先驗證邊界條件的正確性。
假設 C = 8,P = 1.0,線程池的任務完全是密集計算,那麼T = 8。只要 8 個活動線程就能讓 8 個 CPU 飽和,再多也沒用,因為 CPU 資源已經耗光了。
假設 C = 8,P = 0.5,線程池的任務有一半是計算,有一半等在 IO 上,那麼T = 16。考慮操作系統能靈活合理地調度 sleeping/writing/running 線程,那麼大概 16 個「50%繁忙的線程」能讓 8 個 CPU 忙個不停。啟動更多的線程並不能提高吞吐量,反而因為增加上下文切換的開銷而降低性能。
如果 P < 0.2,這個公式就不適用了,T 可以取一個固定值,比如 5*C。

另外,公式里的 C 不一定是 CPU 總數,可以是「分配給這項任務的 CPU 數目」,比如在 8 核機器上分出 4 個核來做一項任務,那麼 C=4。

四:進程間通信只用TCP
Linux下進程間通信的方式有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息隊列、共享內存、信號(signals),以及Socket。同步原語有互斥器(mutex)、條件變數(condition variable)、讀寫鎖(reader-writer lock)、文件鎖(record locking)、信號量(semaphore)等等。

進程間通信我首選Sockets(主要指TCP,我沒有用過UDP,也不考慮Unix domain協議)。其好處在於:
可以跨主機,具有伸縮性。反正都是多進程了,如果一台機器的處理能力不夠,很自然地就能用多台機器來處理。把進程分散到同一區域網的多台機器上,程序改改host:port配置就能繼續用;
TCP sockets和pipe都是操作文件描述符,用來收發位元組流,都可以read/write/fcntl/select/poll等。不同的是,TCP是雙向的,Linux的pipe是單向的,進程間雙向通信還得開兩個文件描述符,不方便;而且進程要有父子關系才能用pipe,這些都限制了pipe的使用;
TCP port由一個進程獨占,且進程退出時操作系統會自動回收文件描述符。因此即使程序意外退出,也不會給系統留下垃圾,程序重啟之後能比較容易地恢復,而不需要重啟操作系統(用跨進程的mutex就有這個風險);而且,port是獨占的,可以防止程序重復啟動,後面那個進程搶不到port,自然就沒法初始化了,避免造成意料之外的結果;
與其他IPC相比,TCP協議的一個天生的好處是「可記錄、可重現」。tcpmp和Wireshark是解決兩個進程間協議和狀態爭端的好幫手,也是性能(吞吐量、延遲)分析的利器。我們可以藉此編寫分布式程序的自動化回歸測試。也可以用tcp之類的工具進行壓力測試。TCP還能跨語言,服務端和客戶端不必使用同一種語言。

分布式系統的軟體設計和功能劃分一般應該以「進程」為單位。從宏觀上看,一個分布式系統是由運行在多台機器上的多個進程組成的,進程之間採用TCP長連接通信。
使用TCP長連接的好處有兩點:一是容易定位分布式系統中的服務之間的依賴關系。只要在機器上運行netstat -tpna|grep <port>就能立刻列出用到某服務的客戶端地址(Foreign Address列),然後在客戶端的機器上用netstat或lsof命令找出是哪個進程發起的連接。TCP短連接和UDP則不具備這一特性。二是通過接收和發送隊列的長度也較容易定位網路或程序故障。在正常運行的時候,netstat列印的Recv-Q和Send-Q都應該接近0,或者在0附近擺動。如果Recv-Q保持不變或持續增加,則通常意味著服務進程的處理速度變慢,可能發生了死鎖或阻塞。如果Send-Q保持不變或持續增加,有可能是對方伺服器太忙、來不及處理,也有可能是網路中間某個路由器或交換機故障造成丟包,甚至對方伺服器掉線,這些因素都可能表現為數據發送不出去。通過持續監控Recv-Q和Send-Q就能及早預警性能或可用性故障。以下是服務端線程阻塞造成Recv-Q和客戶端Send-Q激增的例子:
[cpp] view plain
$netstat -tn
Proto Recv-Q Send-Q Local Address Foreign
tcp 78393 0 10.0.0.10:2000 10.0.0.10:39748 #服務端連接
tcp 0 132608 10.0.0.10:39748 10.0.0.10:2000 #客戶端連接
tcp 0 52 10.0.0.10:22 10.0.0.4:55572

五:多線程伺服器的適用場合
如果要在一台多核機器上提供一種服務或執行一個任務,可用的模式有:
a:運行一個單線程的進程;
b:運行一個多線程的進程;
c:運行多個單線程的進程;
d:運行多個多線程的進程;

考慮這樣的場景:如果使用速率為50MB/s的數據壓縮庫,進程創建銷毀的開銷是800微秒,線程創建銷毀的開銷是50微秒。如何執行壓縮任務?
如果要偶爾壓縮1GB的文本文件,預計運行時間是20s,那麼起一個進程去做是合理的,因為進程啟動和銷毀的開銷遠遠小於實際任務的耗時。
如果要經常壓縮500kB的文本數據,預計運行時間是10ms,那麼每次都起進程 似乎有點浪費了,可以每次單獨起一個線程去做。
如果要頻繁壓縮10kB的文本數據,預計運行時間是200微秒,那麼每次起線程似 乎也很浪費,不如直接在當前線程搞定。也可以用一個線程池,每次把壓縮任務交給線程池,避免阻塞當前線程(特別要避免阻塞IO線程)。
由此可見,多線程並不是萬靈丹(silver bullet)。

1:必須使用單線程的場合
據我所知,有兩種場合必須使用單線程:
a:程序可能會fork(2);
實際編程中,應該保證只有單線程程序能進行fork(2)。多線程程序不是不能調用fork(2),而是這么做會遇到很多麻煩:
fork一般不能在多線程程序中調用,因為Linux的fork只克隆當前線程的thread of control,不可隆其他線程。fork之後,除了當前線程之外,其他線程都消失了。
這就造成一種危險的局面。其他線程可能正好處於臨界區之內,持有了某個鎖,而它突然死亡,再也沒有機會去解鎖了。此時如果子進程試圖再對同一個mutex加鎖,就會立即死鎖。因此,fork之後,子進程就相當於處於signal handler之中(因為不知道調用fork時,父進程中的線程此時正在調用什麼函數,這和信號發生時的場景一樣),你不能調用線程安全的函數(除非它是可重入的),而只能調用非同步信號安全的函數。比如,fork之後,子進程不能調用:
malloc,因為malloc在訪問全局狀態時幾乎肯定會加鎖;
任何可能分配或釋放內存的函數,比如snprintf;
任何Pthreads函數;
printf系列函數,因為其他線程可能恰好持有stdout/stderr的鎖;
除了man 7 signal中明確列出的信號安全函數之外的任何函數。

因此,多線程中調用fork,唯一安全的做法是fork之後,立即調用exec執行另一個程序,徹底隔斷子進程與父進程的聯系。

在多線程環境中調用fork,產生子進程後。子進程內部只存在一個線程,也就是父進程中調用fork的線程的副本。
使用fork創建子進程時,子進程通過繼承整個地址空間的副本,也從父進程那裡繼承了所有互斥量、讀寫鎖和條件變數的狀態。如果父進程中的某個線程佔有鎖,則子進程同樣佔有這些鎖。問題是子進程並不包含佔有鎖的線程的副本,所以子進程沒有辦法知道它佔有了哪些鎖,並且需要釋放哪些鎖。
盡管Pthread提供了pthread_atfork函數試圖繞過這樣的問題,但是這回使得代碼變得混亂。因此《Programming With Posix Threads》一書的作者說:」Avoid using fork in threaded code except where the child process will immediately exec a new program.」。

b:限製程序的CPU佔用率;
這個很容易理解,比如在一個8核的伺服器上,一個單線程程序即便發生busy-wait,占滿1個core,其CPU使用率也只有12.5%,在這種最壞的情況下,系統還是有87.5%的計算資源可供其他服務進程使用。
因此對於一些輔助性的程序,如果它必須和主要服務進程運行在同一台機器的話,那麼做成單線程的能避免過分搶奪系統的計算資源。

㈨ 一個Linux多進程編程

1 引言
對於沒有接觸過Unix/Linux操作系統的人來說,fork是最難理解的概念之一:它執行一次卻返回兩個值。fork函數是Unix系統最傑出的成就之一,它是七十年代UNIX早期的開發者經過長期在理論和實踐上的艱苦探索後取得的成果,一方面,它使操作系統在進程管理上付出了最小的代價,另一方面,又為程序員提供了一個簡潔明了的多進程方法。與DOS和早期的Windows不同,Unix/Linux系統是真正實現多任務操作的系統,可以說,不使用多進程編程,就不能算是真正的Linux環境下編程。
多線程程序設計的概念早在六十年代就被提出,但直到八十年代中期,Unix系統中才引入多線程機制,如今,由於自身的許多優點,多線程編程已經得到了廣泛的應用。
下面,我們將介紹在Linux下編寫多進程和多線程程序的一些初步知識。

2 多進程編程
什麼是一個進程?進程這個概念是針對系統而不是針對用戶的,對用戶來說,他面對的概念是程序。當用戶敲入命令執行一個程序的時候,對系統而言,它將啟動一個進程。但和程序不同的是,在這個進程中,系統可能需要再啟動一個或多個進程來完成獨立的多個任務。多進程編程的主要內容包括進程式控制制和進程間通信,在了解這些之前,我們先要簡單知道進程的結構。

2.1 Linux下進程的結構
Linux下一個進程在內存里有三部分的數據,就是"代碼段"、"堆棧段"和"數據段"。其實學過匯編語言的人一定知道,一般的CPU都有上述三種段寄存器,以方便操作系統的運行。這三個部分也是構成一個完整的執行序列的必要的部分。
"代碼段",顧名思義,就是存放了程序代碼的數據,假如機器中有數個進程運行相同的一個程序,那麼它們就可以使用相同的代碼段。"堆棧段"存放的就是子程序的返回地址、子程序的參數以及程序的局部變數。而數據段則存放程序的全局變數,常數以及動態數據分配的數據空間(比如用malloc之類的函數取得的空間)。這其中有許多細節問題,這里限於篇幅就不多介紹了。系統如果同時運行數個相同的程序,它們之間就不能使用同一個堆棧段和數據段。

2.2 Linux下的進程式控制制
在傳統的Unix環境下,有兩個基本的操作用於創建和修改進程:函數fork( )用來創建一個新的進程,該進程幾乎是當前進程的一個完全拷貝;函數族exec( )用來啟動另外的進程以取代當前運行的進程。Linux的進程式控制制和傳統的Unix進程式控制制基本一致,只在一些細節的地方有些區別,例如在Linux系統中調用vfork和fork完全相同,而在有些版本的Unix系統中,vfork調用有不同的功能。由於這些差別幾乎不影響我們大多數的編程,在這里我們不予考慮。
2.2.1 fork( )
fork在英文中是"分叉"的意思。為什麼取這個名字呢?因為一個進程在運行中,如果使用了fork,就產生了另一個進程,於是進程就"分叉"了,所以這個名字取得很形象。下面就看看如何具體使用fork,這段程序演示了使用fork的基本框架:

void main(){
int i;
if ( fork() == 0 ) {
/* 子進程程序 */
for ( i = 1; i <1000; i ++ ) printf("This is child process\n");
}
else {
/* 父進程程序*/
for ( i = 1; i <1000; i ++ ) printf("This is process process\n");
}
}
程序運行後,你就能看到屏幕上交替出現子進程與父進程各列印出的一千條信息了。如果程序還在運行中,你用ps命令就能看到系統中有兩個它在運行了。
那麼調用這個fork函數時發生了什麼呢?fork函數啟動一個新的進程,前面我們說過,這個進程幾乎是當前進程的一個拷貝:子進程和父進程使用相同的代碼段;子進程復制父進程的堆棧段和數據段。這樣,父進程的所有數據都可以留給子進程,但是,子進程一旦開始運行,雖然它繼承了父進程的一切數據,但實際上數據卻已經分開,相互之間不再有影響了,也就是說,它們之間不再共享任何數據了。它們再要交互信息時,只有通過進程間通信來實現,這將是我們下面的內容。既然它們如此相象,系統如何來區分它們呢?這是由函數的返回值來決定的。對於父進程,fork函數返回了子程序的進程號,而對於子程序,fork函數則返回零。在操作系統中,我們用ps函數就可以看到不同的進程號,對父進程而言,它的進程號是由比它更低層的系統調用賦予的,而對於子進程而言,它的進程號即是fork函數對父進程的返回值。在程序設計中,父進程和子進程都要調用函數fork()下面的代碼,而我們就是利用fork()函數對父子進程的不同返回值用if...else...語句來實現讓父子進程完成不同的功能,正如我們上面舉的例子一樣。我們看到,上面例子執行時兩條信息是交互無規則的列印出來的,這是父子進程獨立執行的結果,雖然我們的代碼似乎和串列的代碼沒有什麼區別。
讀者也許會問,如果一個大程序在運行中,它的數據段和堆棧都很大,一次fork就要復制一次,那麼fork的系統開銷不是很大嗎?其實UNIX自有其解決的辦法,大家知道,一般CPU都是以"頁"為單位來分配內存空間的,每一個頁都是實際物理內存的一個映像,象INTEL的CPU,其一頁在通常情況下是4086位元組大小,而無論是數據段還是堆棧段都是由許多"頁"構成的,fork函數復制這兩個段,只是"邏輯"上的,並非"物理"上的,也就是說,實際執行fork時,物理空間上兩個進程的數據段和堆棧段都還是共享著的,當有一個進程寫了某個數據時,這時兩個進程之間的數據才有了區別,系統就將有區別的"頁"從物理上也分開。系統在空間上的開銷就可以達到最小。
下面演示一個足以"搞死"Linux的小程序,其源代碼非常簡單:
void main()
{
for( ; ; ) fork();
}
這個程序什麼也不做,就是死循環地fork,其結果是程序不斷產生進程,而這些進程又不斷產生新的進程,很快,系統的進程就滿了,系統就被這么多不斷產生的進程"撐死了"。當然只要系統管理員預先給每個用戶設置可運行的最大進程數,這個惡意的程序就完成不了企圖了。
2.2.2 exec( )函數族
下面我們來看看一個進程如何來啟動另一個程序的執行。在Linux中要使用exec函數族。系統調用execve()對當前進程進行替換,替換者為一個指定的程序,其參數包括文件名(filename)、參數列表(argv)以及環境變數(envp)。exec函數族當然不止一個,但它們大致相同,在Linux中,它們分別是:execl,execlp,execle,execv,execve和execvp,下面我只以execlp為例,其它函數究竟與execlp有何區別,請通過manexec命令來了解它們的具體情況。
一個進程一旦調用exec類函數,它本身就"死亡"了,系統把代碼段替換成新的程序的代碼,廢棄原有的數據段和堆棧段,並為新程序分配新的數據段與堆棧段,唯一留下的,就是進程號,也就是說,對系統而言,還是同一個進程,不過已經是另一個程序了。(不過exec類函數中有的還允許繼承環境變數之類的信息。)
那麼如果我的程序想啟動另一程序的執行但自己仍想繼續運行的話,怎麼辦呢?那就是結合fork與exec的使用。下面一段代碼顯示如何啟動運行其它程序:

char command[256];
void main()
{
int rtn; /*子進程的返回數值*/
while(1) {
/* 從終端讀取要執行的命令 */
printf( ">" );
fgets( command, 256, stdin );
command[strlen(command)-1] = 0;
if ( fork() == 0 ) {
/* 子進程執行此命令 */
execlp( command, command );
/* 如果exec函數返回,表明沒有正常執行命令,列印錯誤信息*/
perror( command );
exit( errorno );
}
else {
/* 父進程, 等待子進程結束,並列印子進程的返回值 */
wait ( &rtn );
printf( " child process return %d\n",. rtn );
}
}
}

此程序從終端讀入命令並執行之,執行完成後,父進程繼續等待從終端讀入命令。熟悉DOS和WINDOWS系統調用的朋友一定知道DOS/WINDOWS也有exec類函數,其使用方法是類似的,但DOS/WINDOWS還有spawn類函數,因為DOS是單任務的系統,它只能將"父進程"駐留在機器內再執行"子進程",這就是spawn類的函數。WIN32已經是多任務的系統了,但還保留了spawn類函數,WIN32中實現spawn函數的方法同前述UNIX中的方法差不多,開設子進程後父進程等待子進程結束後才繼續運行。UNIX在其一開始就是多任務的系統,所以從核心角度上講不需要spawn類函數。
在這一節里,我們還要講講system()和popen()函數。system()函數先調用fork(),然後再調用exec()來執行用戶的登錄shell,通過它來查找可執行文件的命令並分析參數,最後它么使用wait()函數族之一來等待子進程的結束。函數popen()和函數system()相似,不同的是它調用pipe()函數創建一個管道,通過它來完成程序的標准輸入和標准輸出。這兩個函數是為那些不太勤快的程序員設計的,在效率和安全方面都有相當的缺陷,在可能的情況下,應該盡量避免。

2.3 Linux下的進程間通信
詳細的講述進程間通信在這里絕對是不可能的事情,而且筆者很難有信心說自己對這一部分內容的認識達到了什麼樣的地步,所以在這一節的開頭首先向大家推薦著名作者Richard Stevens的著名作品:《Advanced Programming in the UNIX Environment》,它的中文譯本《UNIX環境高級編程》已有機械工業出版社出版,原文精彩,譯文同樣地道,如果你的確對在Linux下編程有濃厚的興趣,那麼趕緊將這本書擺到你的書桌上或計算機旁邊來。說這么多實在是難抑心中的景仰之情,言歸正傳,在這一節里,我們將介紹進程間通信最最初步和最最簡單的一些知識和概念。
首先,進程間通信至少可以通過傳送打開文件來實現,不同的進程通過一個或多個文件來傳遞信息,事實上,在很多應用系統里,都使用了這種方法。但一般說來,進程間通信(IPC:InterProcess Communication)不包括這種似乎比較低級的通信方法。Unix系統中實現進程間通信的方法很多,而且不幸的是,極少方法能在所有的Unix系統中進行移植(唯一一種是半雙工的管道,這也是最原始的一種通信方式)。而Linux作為一種新興的操作系統,幾乎支持所有的Unix下常用的進程間通信方法:管道、消息隊列、共享內存、信號量、套介面等等。下面我們將逐一介紹。
2.3.1 管道
管道是進程間通信中最古老的方式,它包括無名管道和有名管道兩種,前者用於父進程和子進程間的通信,後者用於運行於同一台機器上的任意兩個進程間的通信。
無名管道由pipe()函數創建:
#include <unistd.h>
int pipe(int filedis[2]);
參數filedis返回兩個文件描述符:filedes[0]為讀而打開,filedes[1]為寫而打開。filedes[1]的輸出是filedes[0]的輸入。下面的例子示範了如何在父進程和子進程間實現通信。

#define INPUT 0
#define OUTPUT 1

void main() {
int file_descriptors[2];
/*定義子進程號 */
pid_t pid;
char buf[256];
int returned_count;
/*創建無名管道*/
pipe(file_descriptors);
/*創建子進程*/
if((pid = fork()) == -1) {
printf("Error in fork\n");
exit(1);
}
/*執行子進程*/
if(pid == 0) {
printf("in the spawned (child) process...\n");
/*子進程向父進程寫數據,關閉管道的讀端*/
close(file_descriptors[INPUT]);
write(file_descriptors[OUTPUT], "test data", strlen("test data"));
exit(0);
} else {
/*執行父進程*/
printf("in the spawning (parent) process...\n");
/*父進程從管道讀取子進程寫的數據,關閉管道的寫端*/
close(file_descriptors[OUTPUT]);
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
printf("%d bytes of data received from spawned process: %s\n",
returned_count, buf);
}
}
在Linux系統下,有名管道可由兩種方式創建:命令行方式mknod系統調用和函數mkfifo。下面的兩種途徑都在當前目錄下生成了一個名為myfifo的有名管道:
方式一:mkfifo("myfifo","rw");
方式二:mknod myfifo p
生成了有名管道後,就可以使用一般的文件I/O函數如open、close、read、write等來對它進行操作。下面即是一個簡單的例子,假設我們已經創建了一個名為myfifo的有名管道。
/* 進程一:讀有名管道*/
#include <stdio.h>
#include <unistd.h>
void main() {
FILE * in_file;
int count = 1;
char buf[80];
in_file = fopen("mypipe", "r");
if (in_file == NULL) {
printf("Error in fdopen.\n");
exit(1);
}
while ((count = fread(buf, 1, 80, in_file)) > 0)
printf("received from pipe: %s\n", buf);
fclose(in_file);
}
/* 進程二:寫有名管道*/
#include <stdio.h>
#include <unistd.h>
void main() {
FILE * out_file;
int count = 1;
char buf[80];
out_file = fopen("mypipe", "w");
if (out_file == NULL) {
printf("Error opening pipe.");
exit(1);
}
sprintf(buf,"this is test data for the named pipe example\n");
fwrite(buf, 1, 80, out_file);
fclose(out_file);
}

2.3.2 消息隊列
消息隊列用於運行於同一台機器上的進程間通信,它和管道很相似,事實上,它是一種正逐漸被淘汰的通信方式,我們可以用流管道或者套介面的方式來取代它,所以,我們對此方式也不再解釋,也建議讀者忽略這種方式。

2.3.3 共享內存
共享內存是運行在同一台機器上的進程間通信最快的方式,因為數據不需要在不同的進程間復制。通常由一個進程創建一塊共享內存區,其餘進程對這塊內存區進行讀寫。得到共享內存有兩種方式:映射/dev/mem設備和內存映像文件。前一種方式不給系統帶來額外的開銷,但在現實中並不常用,因為它控制存取的將是實際的物理內存,在Linux系統下,這只有通過限制Linux系統存取的內存才可以做到,這當然不太實際。常用的方式是通過shmXXX函數族來實現利用共享內存進行存儲的。
首先要用的函數是shmget,它獲得一個共享存儲標識符。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int flag);
這個函數有點類似大家熟悉的malloc函數,系統按照請求分配size大小的內存用作共享內存。Linux系統內核中每個IPC結構都有的一個非負整數的標識符,這樣對一個消息隊列發送消息時只要引用標識符就可以了。這個標識符是內核由IPC結構的關鍵字得到的,這個關鍵字,就是上面第一個函數的key。數據類型key_t是在頭文件sys/types.h中定義的,它是一個長整形的數據。在我們後面的章節中,還會碰到這個關鍵字。
當共享內存創建後,其餘進程可以調用shmat()將其連接到自身的地址空間中。
void *shmat(int shmid, void *addr, int flag);
shmid為shmget函數返回的共享存儲標識符,addr和flag參數決定了以什麼方式來確定連接的地址,函數的返回值即是該進程數據段所連接的實際地址,進程可以對此進程進行讀寫操作。
使用共享存儲來實現進程間通信的注意點是對數據存取的同步,必須確保當一個進程去讀取數據時,它所想要的數據已經寫好了。通常,信號量被要來實現對共享存儲數據存取的同步,另外,可以通過使用shmctl函數設置共享存儲內存的某些標志位如SHM_LOCK、SHM_UNLOCK等來實現。

㈩ linux下C語言socket編程雙機互發數據

這個問題很好辦啦,伺服器接受一個連接請求,然後開一個線程或者進程都可以,再在線程或者進程裡面採用其他技術實現同時收發(比如I/O復用,比如非阻塞I/O)。客戶端也可以採用I/O復用。

推薦資料的話,《unix網路編程》這本書很好,公認的經典,當教科書用,這本書里有你想要的所有內容。

ps:你基礎太差,多補補吧,別想一下吃個胖子。

另外我這里正好有個例子滿足你的要求,貼給你,自己寫的,不是網上找的,用的是多進程加I/O復用技術:

server端:
/****************************************************************
**
**
**
****************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

#define BUFLEN 1024
#define MAX(a,b) ((a)>(b)?(a):(b))

typedef void Sigfunc (int);

void str_echo(FILE *,int);
//Sigfunc *signal(int, Sigfunc *);

int main(int argc,char **argv)
{
int connfd,listenfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
void sig_chld(int);

listenfd = socket(AF_INET, SOCK_STREAM, 0);

memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(5358);

bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,8);

signal(SIGCHLD,sig_chld);

while(1)
{
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen)) < 0)
{
if(errno == EINTR)
{
fputs("accept error: EINTR\n",stdout);
continue;
}
else
{
fputs("accept error..\n",stdout);
}
}

if((childpid = fork()) == 0)
{
close(listenfd);
str_echo(stdin,connfd);
exit(0);
}
close(connfd);
}
}

void str_echo(FILE *fp,int sockfd)
{
int n = 0;
char sendbuf[BUFLEN] = { 0 },recvbuf[BUFLEN] = { 0 };
int maxfdp;
fd_set rset;

FD_ZERO(&rset);

while(1)
{
FD_SET(fileno(fp),&rset);
FD_SET(sockfd, &rset);
maxfdp = MAX(fileno(fp),sockfd)+1;

select(maxfdp, &rset ,NULL, NULL, NULL);

if(FD_ISSET(sockfd, &rset))
{
if(n = read(sockfd, recvbuf, BUFLEN) == 0)
{
return;
}
if(n == -1)
{
break;
}
printf("%s\n",recvbuf);
memset(recvbuf,0,BUFLEN);
}
if(FD_ISSET(fileno(fp),&rset))
{
scanf("%s",sendbuf);
write(sockfd, sendbuf,strlen(sendbuf));
}
}
}

void sig_chld (int signo)
{
pid_t pid;
int stat;

while ((pid = waitpid(-1,&stat, WNOHANG)) > 0)
{
printf("child %d terminated\n",pid);
}
return;
}

client端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

#define MAX(a,b) (a)>(b)?(a):(b)

int main()
{
int s,connectReturn, maxfd;
fd_set rset;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
long port=5358;
s=socket(PF_INET,SOCK_STREAM,0);

struct sockaddr_in sa;
sa.sin_family=AF_INET;
sa.sin_addr.s_addr=inet_addr("127.0.0.1");
sa.sin_port=htons(port);
connectReturn=connect(s,(struct sockaddr *)&sa,sizeof(sa));
printf("%d\n",connectReturn);
FD_ZERO(&rset);
while(1)
{
FD_SET(fileno(stdin), &rset);
FD_SET(s, &rset);
maxfd=MAX(fileno(stdin), s) + 1;

select(maxfd, &rset, NULL, NULL, NULL);
if(FD_ISSET(fileno(stdin), &rset))
{
scanf("%s",sendbuf);
send(s,sendbuf,strlen(sendbuf),0);
bzero(sendbuf, 1024);
}
else if(FD_ISSET(s, &rset))
{
memset(recvbuf,0,1024);
recv(s,recvbuf,1024,0);
printf("remote: %s\n",recvbuf);
}
}
return 0;
}

熱點內容
內置存儲卡可以拆嗎 發布:2025-05-18 04:16:35 瀏覽:336
編譯原理課時設置 發布:2025-05-18 04:13:28 瀏覽:378
linux中進入ip地址伺服器 發布:2025-05-18 04:11:21 瀏覽:612
java用什麼軟體寫 發布:2025-05-18 03:56:19 瀏覽:32
linux配置vim編譯c 發布:2025-05-18 03:55:07 瀏覽:107
砸百鬼腳本 發布:2025-05-18 03:53:34 瀏覽:945
安卓手機如何拍視頻和蘋果一樣 發布:2025-05-18 03:40:47 瀏覽:742
為什麼安卓手機連不上蘋果7熱點 發布:2025-05-18 03:40:13 瀏覽:803
網卡訪問 發布:2025-05-18 03:35:04 瀏覽:511
接收和發送伺服器地址 發布:2025-05-18 03:33:48 瀏覽:372