一篇比較不錯的文章, 降到了 makefile make , gcc編譯器,GDB調試器,html
Linux文件系統,Linux文件API,.C語言庫函數(C庫函數的文件操做其實是獨立於具體的操做系統平臺的),進程控制與進程通訊編程node
Linux進程在內存中包含三部分數據:代碼段、堆棧段和數據段。代碼段存放了程序的代碼。代碼段能夠爲機器中運行同一程序的數個linux
進程共享。堆棧段存放的是子程序(函數)的返回地址、子程序的參數及程序的局部變量。而數據段則存放程序的全局變量、常數以及動態數shell
據分配的數據空間(好比用malloc函數申請的內存)。與代碼段不一樣,若是系統中同時運行多個相同的程序,它們不能使用同一堆棧段和數據編程
段。ubuntu
Linux進程主要有以下幾種狀態:用戶狀態(進程在用戶狀態下運行的狀態)、內核狀態(進程在內核狀態下運行的狀態)、內存中就緒(進程api
沒有執行,但處於就緒狀態,只要內核調度它,就能夠執行)、內存中睡眠(進程正在睡眠而且處於內存中,沒有被交換到SWAP設備)、就緒數組
且換出(進程處於就緒狀態,可是必須把它換入內存,內核才能再次調度它進行運行)、睡眠且換出(進程正在睡眠,且被換出內存)、被搶安全
先(進程從內核狀態返回用戶狀態時,內核搶先於它,作了上下文切換,調度了另外一個進程,原先這個進程就處於被搶先狀態)、建立狀態(服務器
進程剛被建立,該進程存在,但既不是就緒狀態,也不是睡眠狀態,這個狀態是除了進程0之外的全部進程的最初狀態)、僵死狀態(進程調用
exit結束,進程再也不存在,但在進程表項中仍有記錄,該記錄可由父進程收集)。
下面咱們來以一個進程從建立到消亡的過程講解Linux進程狀態轉換的「生死因果」。
(1)進程被父進程經過系統調用fork建立而處於建立態;
(2)fork調用爲子進程配置好內核數據結構和子進程私有數據結構後,子進程進入就緒態(或者在內存中就緒,或者由於內存不夠而在SWAP設
備中就緒);
(3)若進程在內存中就緒,進程能夠被內核調度程序調度到CPU運行;
(4)內核調度該進程進入內核狀態,再由內核狀態返回用戶狀態執行。該進程在用戶狀態運行必定時間後,又會被調度程序所調度而進入內核
狀態,由此轉入就緒態。有時進程在用戶狀態運行時,也會由於須要內核服務,使用系統調用而進入內核狀態,服務完畢,會由內核狀態轉回
用戶狀態。要注意的是,進程在從內核狀態向用戶狀態返回時可能被搶佔,這是因爲有優先級更高的進程急需使用CPU,不能等到下一次調度時
機,從而形成搶佔;
(5)進程執行exit調用,進入僵死狀態,最終結束。
進程控制中主要涉及到進程的建立、睡眠和退出等,在Linux中主要提供了fork、exec、clone的進程建立方法,sleep的進程睡眠和exit的進程
退出調用,另外Linux還提供了父進程等待子進程結束的系統調用wait。
fork
對於沒有接觸過Unix/Linux操做系統的人來講,fork是最難理解的概念之一,它執行一次卻返回兩個值,徹底「難以想象」。先看下面的程序
:
int main()
{
int i;
if (fork() == 0)
{
for (i = 1; i < 3; i++)
printf("This is child process"n");
}
else
{
for (i = 1; i < 3; i++)
printf("This is parent process"n");
}
}
執行結果爲:
This is child process
This is child process
This is parent process
This is parent process
fork在英文中是「分叉」的意思,這個名字取得很形象。一個進程在運行中,若是使用了fork,就產生了另外一個進程,因而進程就「分叉」了
。當前進程爲父進程,經過fork()會產生一個子進程。對於父進程,fork函數返回子程序的進程號而對於子程序,fork函數則返回零,這就是
一個函數返回兩次的本質。能夠說,fork函數是Unix系統最傑出的成就之一,它是七十年代Unix早期的開發者通過理論和實踐上的長期艱苦探
索後取得的成果。
Linux的進程間通訊(IPC,InterProcess Communication)通訊方法有管道、消息隊列、共享內存、信號量、套接口等。
管道分爲有名管道和無名管道,無名管道只能用於親屬進程之間的通訊,而有名管道則可用於無親屬關係的進程之間。
--------------------------------------------------------------------------
Linux下的C編程實戰(一)
――開發平臺搭建
1.引言
Linux操做系統在服務器領域的應用和普及已經有較長的歷史,這源於它的開源特色以及其超越Windows的安全性和穩定性。而近年來,
Linux操做系統在嵌入式系統領域的延伸也可謂是如日中天,許多版本的嵌入式Linux系統被開發出來,如ucLinux、RTLinux、ARM-Linux等等。
在嵌入式操做系統方面,Linux的地位是不容懷疑的,它開源、它包含TCP/IP協議棧、它易集成GUI。
鑑於Linux操做系統在服務器和嵌入式系統領域越來越普遍的應用,社會上愈來愈須要基於Linux操做系統進行編程的開發人員。
瀏覽許多論壇,常常碰到這樣的提問:「如今是否是很流行unix/linux下的c編程?因此想學習一下!可是不知道該從何學起,如何下手!有什
麼好的建議嗎?各位高手!哪些書籍比較合適初學者?在深刻淺出的過程當中應該看哪些不一樣層次的書?好比好的網站、論壇請你們賜教!不慎
感激!」
鑑於讀者的需求,在本文中,筆者將對Linux平臺下C編程的幾個方面進行實例講解,併力求回答讀者們關心的問題,以與讀者朋友們進行交流
,共同提升。在本文的連載過程當中,有任何問題或建議,您能夠給筆者發送email:21cnbao@21cn.com,您也能夠進入筆者的博客參與討論:
http://blog.donews.com/21cnbao。
筆者建議在PC內存足夠大的狀況下,不要直接安裝Linux操做系統,最好把它安裝在運行VMWare虛擬機軟件的Windows平臺上,以下圖:
在Linux平臺下,可用任意一個文本編輯工具編輯源代碼,但筆者建議使用emacs軟件,它具有語法高亮、版本控制等附帶功能,以下圖
:
2.GCC編譯器
GCC是Linux平臺下最重要的開發工具,它是GNU的C和C++編譯器,其基本用法爲:
gcc [options] [filenames]
options爲編譯選項,GCC總共提供的編譯選項超過100個,但只有少數幾個會被頻繁使用,咱們僅對幾個經常使用選項進行介紹。
假設咱們編譯一輸出「Hello World」的程序:
/* Filename:helloworld.c */
main()
{
printf("Hello World"n");
}
最簡單的編譯方法是不指定任何編譯選項:
gcc helloworld.c
它會爲目標程序生成默認的文件名a.out,咱們可用-o編譯選項來爲將產生的可執行文件指定一個文件名來代替a.out。例如,將上述名爲
helloworld.c的C程序編譯爲名叫helloworld的可執行文件,須要輸入以下命令:
gcc –o helloworld helloworld.c
-c選項告訴GCC僅把源代碼編譯爲目標代碼而跳過彙編和鏈接的步驟;
-S 編譯選項告訴GCC 在爲 C代碼產生了彙編語言文件後中止編譯。GCC 產生的彙編語言文件的缺省擴展名是.s,上述程序運行以下命令:
gcc –S helloworld.c
將生成helloworld.c的彙編代碼,使用的是AT&T彙編。用emacs打開彙編代碼以下圖:
-E選項指示編譯器僅對輸入文件進行預處理。當這個選項被使用時,預處理器的輸出被送到標準輸出(默認爲屏幕)而不是儲存在文件裏。
-O選項告訴GCC對源代碼進行基本優化從而使得程序執行地更快;而-O2選項告訴GCC產生儘量小和儘量快的代碼。使用-O2選項編譯的速度
比使用-O時慢,但產生的代碼執行速度會更快。
-g選項告訴GCC產生能被GNU調試器使用的調試信息以便調試你的程序,可喜的是,在GCC裏,咱們能聯用-g和-O (產生優化代碼)。
-pg選項告訴GCC在你的程序里加入額外的代碼,執行時,產生gprof用的剖析信息以顯示你的程序的耗時狀況。
3.GDB調試器
GCC用於編譯程序,而Linux的另外一個GNU工具gdb則用於調試程序。gdb是一個用來調試C和C++程序的強力調試器,咱們能經過它進行一
系列調試工做,包括設置斷點、觀查變量、單步等。
其最經常使用的命令以下:
file:裝入想要調試的可執行文件。
kill:終止正在調試的程序。
list:列表顯示源代碼。
next:執行一行源代碼但不進入函數內部。
step:執行一行源代碼並且進入函數內部。
run:執行當前被調試的程序
quit:終止gdb
watch:監視一個變量的值
break:在代碼裏設置斷點,程序執行到這裏時掛起
make:不退出gdb而從新產生可執行文件
shell:不離開gdb而執行shell
下面咱們來演示怎樣用GDB來調試一個求0+1+2+3+…+99的程序:
/* Filename:sum.c */
main()
{
int i, sum;
sum = 0;
for (i = 0; i < 100; i++)
{
sum + = i;
}
printf("the sum of 1+2+...+ is %d", sum);
}
執行以下命令編譯sum.c(加-g選項產生debug信息):
gcc –g –o sum sum.c
在命令行上鍵入gdb sum並按回車鍵就能夠開始調試sum了,再運行run命令執行sum,屏幕上將看到以下內容:
list命令:
list命令用於列出源代碼,對上述程序兩次運行list,將出現以下畫面(源代碼被標行號):
根據列出的源程序,若是咱們將斷點設置在第5行,只需在gdb 命令行提示符下鍵入以下命令設置斷點:(gdb) break 5,執行狀況以下圖:
這個時候咱們再run,程序會中止在第5行,以下圖:
設置斷點的另外一種語法是 break ,它在進入指定函數(function)時停住。
相反的,clear用於清除全部的已定義的斷點,clear 清除設置在函數上的斷點, clear 則清除設置在指定行上的斷點
。
watch命令:
watch命令用於觀查變量或表達式的值,咱們觀查sum變量只須要運行watch sum:
watch 爲表達式(變量)expr設置一個觀察點,一量表達式值有變化時,程序會中止執行。
要觀查當前設置的watch,可使用info watchpoints命令。
next、step命令:
next、step用於單步執行,在執行的過程當中,被watch變量的變化狀況將實時呈現(分別顯示Old value和New value),以下圖:
next、step命令的區別在於step遇到函數調用,會跳轉到到該函數定義的開始行去執行,而next則不進入到函數內部,它把函數調用語句看成
一條普通語句執行。
4.Make
make是全部想在Linux系統上編程的用戶必須掌握的工具,對於任何稍具規模的程序,咱們都會使用到make,幾乎能夠說不使用make的程序不具
備任何實用價值。
在此,咱們有必要解釋編譯和鏈接的區別。編譯器使用源碼文件來產生某種形式的目標文件(object files),在編譯過程當中,外部的符號參考
並無被解釋或替換(即外部全局變量和函數並無被找到)。所以,在編譯階段所報的錯誤通常都是語法錯誤。而鏈接器則用於鏈接目標文
件和程序包,生成一個可執行程序。在鏈接階段,一個目標文件中對別的文件中的符號的參考被解釋,若是有符號不能找到,會報告鏈接錯誤
。
編譯和鏈接的通常步驟是:第一階段把源文件一個一個的編譯成目標文件,第二階段把全部的目標文件加上須要的程序包鏈接成一個可執行文
件。這樣的過程很痛苦,咱們須要使用大量的gcc命令。
而make則使咱們從大量源文件的編譯和鏈接工做中解放出來,綜合爲一步完成。GNU Make的主要工做是讀進一個文本文件,稱爲makefile。這
個文件記錄了哪些文件(目的文件,目的文件不必定是最後的可執行程序,它能夠是任何一種文件)由哪些文件(依靠文件)產生,用什麼命
令來產生。Make依靠此makefile中的信息檢查磁盤上的文件,若是目的文件的建立或修改時間比它的一個依靠文件舊的話,make就執行相應的
命令,以便更新目的文件。
假設咱們寫下以下的三個文件,add.h用於聲明add函數,add.c提供兩個整數相加的函數體,而main.c中調用add函數:
/* filename:add.h */
extern int add(int i, int j);
/* filename:add.c */
int add(int i, int j)
{
return i + j;
};
/* filename:main.c */
#include "add.h"
main()
{
int a, b;
a = 2;
b = 3;
printf("the sum of a+b is %d", add(a + b));
};
怎樣爲上述三個文件產生makefile呢?以下:
-------------------------
test : main.o add.o
gcc main.o add.o -o test
main.o : main.c add.h
gcc -c main.c -o main.o
add.o : add.c add.h
gcc -c add.c -o add.o
-----------------------
(注意分割符爲TAB鍵)
上述makefile利用add.c和add.h文件執行gcc -c add.c -o add.o命令產生add.o目標代碼,利用main.c和add.h文件執行gcc -c main.c -o
main.o命令產生main.o目標代碼,最後利用main.o和add.o文件(兩個模塊的目標代碼)執行gcc main.o add.o -o test命令產生可執行文件
test。
咱們可在makefile中加入變量,另外。環境變量在make過程當中也被解釋成make的變量。這些變量是大小寫敏感的,通常使用大寫字母。Make變
量能夠作不少事情,例如:
i) 存儲一個文件名列表;
ii) 存儲可執行文件名;
iii) 存儲編譯器選項。
要定義一個變量,只須要在一行的開始寫下這個變量的名字,後面跟一個=號,再跟變量的值。引用變量的方法是寫一個$符號,後面跟(變量
名)。咱們把前面的 makefile 利用變量重寫一遍(並假設使用-Wall -O –g編譯選項):
OBJS = main.o add.o
CC = gcc
CFLAGS = -Wall -O -g
test : $(OBJS)
$(CC) $(OBJS) -o test
main.o : main.c add.h
$(CC) $(CFLAGS) -c main.c -o main.o
add.o : add.c add.h
$(CC) $(CFLAGS) -c add.c -o add.o
makefile 中還可定義清除(clean)目標,可用來清除編譯過程當中產生的中間文件,例如在上述makefile文件中添加下列代碼:
clean:
rm -f *.o
運行make clean時,將執行rm -f *.o命令,刪除全部編譯過程當中產生的中間文件。
無論怎麼說,本身動手編寫makefile仍然是很複雜和煩瑣的,並且很容易出錯。所以,GNU也爲咱們提供了Automake和Autoconf來輔助快速自動
產生makefile,讀者能夠參閱相關資料。
5.小結
本章主要闡述了Linux程序的編寫、編譯、調試方法及make,實際上就是引導讀者學習怎樣在Linux下編程,爲後續章節作好準備。
Linux下的C編程實戰(二)
――文件系統編程
1.Linux文件系統
Linux支持多種文件系統,如ext、ext二、minix、iso9660、msdos、fat、vfat、nfs等。在這些具體文件系統的上層,Linux提供了虛擬
文件系統(VFS)來統一它們的行爲,虛擬文件系統爲不一樣的文件系統與內核的通訊提供了一致的接口。下圖給出了Linux中文件系統的關係:
在Linux平臺下對文件編程可使用兩類函數:(1)Linux操做系統文件API;(2)C語言I/O庫函數。 前者依賴於Linux系統調用,
後者實際上與操做系統是獨立的,由於在任何操做系統下,使用C語言I/O庫函數操做文件的方法都是相同的。本章將對這兩種方法進行實例講
解。
2.Linux文件API
Linux的文件操做API涉及到建立、打開、讀寫和關閉文件。
建立
int creat(const char *filename, mode_t mode);
參數mode指定新建文件的存取權限,它同umask一塊兒決定文件的最終權限(mode&umask),其中umask表明了文件在建立時須要去掉的一些存取
權限。umask可經過系統調用umask()來改變:
int umask(int newmask);
該調用將umask設置爲newmask,而後返回舊的umask,它隻影響讀、寫和執行權限。
打開
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open函數有兩個形式,其中pathname是咱們要打開的文件名(包含路徑名稱,缺省是認爲在當前路徑下面),flags能夠去下面的一個值或者是幾
個值的組合:
標誌
含義
O_RDONLY
以只讀的方式打開文件
O_WRONLY
以只寫的方式打開文件
O_RDWR
以讀寫的方式打開文件
O_APPEND
以追加的方式打開文件
O_CREAT
建立一個文件
O_EXEC
若是使用了O_CREAT並且文件已經存在,就會發生一個錯誤
O_NOBLOCK
以非阻塞的方式打開一個文件
O_TRUNC
若是文件已經存在,則刪除文件的內容
O_RDONLY、O_WRONLY、O_RDWR三個標誌只能使用任意的一個。
若是使用了O_CREATE標誌,則使用的函數是int open(const char *pathname,int flags,mode_t mode); 這個時候咱們還要指定mode標誌,用
來表示文件的訪問權限。mode能夠是如下狀況的組合:
標誌
含義
S_IRUSR
用戶能夠讀
S_IWUSR
用戶能夠寫
S_IXUSR
用戶能夠執行
S_IRWXU
用戶能夠讀、寫、執行
S_IRGRP
組能夠讀
S_IWGRP
組能夠寫
S_IXGRP
組能夠執行
S_IRWXG
組能夠讀寫執行
S_IROTH
其餘人能夠讀
S_IWOTH
其餘人能夠寫
S_IXOTH
其餘人能夠執行
S_IRWXO
其餘人能夠讀、寫、執行
S_ISUID
設置用戶執行ID
S_ISGID
設置組的執行ID
除了能夠經過上述宏進行「或」邏輯產生標誌之外,咱們也能夠本身用數字來表示,Linux總共用5個數字來表示文件的各類權限:第一位表示
設置用戶ID;第二位表示設置組ID;第三位表示用戶本身的權限位;第四位表示組的權限;最後一位表示其餘人的權限。每一個數字能夠取1(執
行權限)、2(寫權限)、4(讀權限)、0(無)或者是這些值的和。例如,要建立一個用戶可讀、可寫、可執行,可是組沒有權限,其餘人能夠讀、
能夠執行的文件,並設置用戶ID位。那麼,咱們應該使用的模式是1(設置用戶ID)、0(不設置組ID)、7(1+2+4,讀、寫、執行)、0(沒有權限)、
5(1+4,讀、執行)即10705:
open("test", O_CREAT, 10705);
上述語句等價於:
open("test", O_CREAT, S_IRWXU | S_IROTH | S_IXOTH | S_ISUID );
若是文件打開成功,open函數會返回一個文件描述符,之後對該文件的全部操做就能夠經過對這個文件描述符進行操做來實現。
讀寫
在文件打開之後,咱們纔可對文件進行讀寫了,Linux中提供文件讀寫的系統調用是read、write函數:
int read(int fd, const void *buf, size_t length);
int write(int fd, const void *buf, size_t length);
其中參數buf爲指向緩衝區的指針,length爲緩衝區的大小(以字節爲單位)。函數read()實現從文件描述符fd所指定的文件中讀取length個字
節到buf所指向的緩衝區中,返回值爲實際讀取的字節數。函數write實現將把length個字節從buf指向的緩衝區中寫到文件描述符fd所指向的文
件中,返回值爲實際寫入的字節數。
以O_CREAT爲標誌的open實際上實現了文件建立的功能,所以,下面的函數等同creat()函數:
int open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);
定位
對於隨機文件,咱們能夠隨機的指定位置讀寫,使用以下函數進行定位:
int lseek(int fd, offset_t offset, int whence);
lseek()將文件讀寫指針相對whence移動offset個字節。操做成功時,返回文件指針相對於文件頭的位置。參數whence可以使用下述值:
SEEK_SET:相對文件開頭
SEEK_CUR:相對文件讀寫指針的當前位置
SEEK_END:相對文件末尾
offset可取負值,例以下述調用可將文件指針相對當前位置向前移動5個字節:
lseek(fd, -5, SEEK_CUR);
因爲lseek函數的返回值爲文件指針相對於文件頭的位置,所以下列調用的返回值就是文件的長度:
lseek(fd, 0, SEEK_END);
關閉
當咱們操做完成之後,咱們要關閉文件了,只要調用close就能夠了,其中fd是咱們要關閉的文件描述符:
int close(int fd);
例程:編寫一個程序,在當前目錄下建立用戶可讀寫文件「hello.txt」,在其中寫入「Hello, software weekly」,關閉該文件。再次打開該
文件,讀取其中的內容並輸出在屏幕上。
#include
#include
#include
#include
#define LENGTH 100
main()
{
int fd, len;
char str[LENGTH];
fd = open("hello.txt", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); /* 建立並打開文件 */
if (fd)
{
write(fd, "Hello, Software Weekly", strlen("Hello, software weekly")); /* 寫入Hello, software weekly字符串 */
close(fd);
}
fd = open("hello.txt", O_RDWR);
len = read(fd, str, LENGTH); /* 讀取文件內容 */
str[len] = '"0';
printf("%s"n", str);
close(fd);
};
編譯並運行,執行
3.C語言庫函數
C庫函數的文件操做其實是獨立於具體的操做系統平臺的,無論是在DOS、Windows、Linux仍是在VxWorks中都是這些函數:
建立和打開
FILE *fopen(const char *path, const char *mode);
fopen()實現打開指定文件filename,其中的mode爲打開模式,C語言中支持的打開模式以下表:
標誌
含義
r, rb
以只讀方式打開
w, wb
以只寫方式打開。若是文件不存在,則建立該文件,不然文件被截斷
a, ab
以追加方式打開。若是文件不存在,則建立該文件
r+, r+b, rb+
以讀寫方式打開
w+, w+b, wh+
以讀寫方式打開。若是文件不存在時,建立新文件,不然文件被截斷
a+, a+b, ab+
以讀和追加方式打開。若是文件不存在,建立新文件
其中b用於區分二進制文件和文本文件,這一點在DOS、Windows系統中是有區分的,但Linux不區分二進制文件和文本文件。
讀寫
C庫函數支持以字符、字符串等爲單位,支持按照某中格式進行文件的讀寫,這一組函數爲:
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
char *fgets(char *s, int n, FILE *stream);
int fputs(const char *s, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);
int fscanf (FILE *stream, const char *format, ...);
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
size_t fwrite (const void *ptr, size_t size, size_t n, FILE *stream);
fread()實現從流stream中讀取加n個字段,每一個字段爲size字節,並將讀取的字段放入ptr所指的字符數組中,返回實際已讀取的字段數。在讀
取的字段數小於num時,多是在函數調用時出現錯誤,也多是讀到文件的結尾。因此要經過調用feof()和ferror()來判斷。
write()實現從緩衝區ptr所指的數組中把n個字段寫到流stream中,每一個字段長爲size個字節,返回實際寫入的字段數。
另外,C庫函數還提供了讀寫過程當中的定位能力,這些函數包括
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
int fseek(FILE *stream, long offset, int whence);
等。
關閉
利用C庫函數關閉文件依然是很簡單的操做:
int fclose (FILE *stream);
例程:將第2節中的例程用C庫函數來實現。
#include
#define LENGTH 100
main()
{
FILE *fd;
char str[LENGTH];
fd = fopen("hello.txt", "w+"); /* 建立並打開文件 */
if (fd)
{
fputs("Hello, Software Weekly", fd); /* 寫入Hello, software weekly字符串 */
fclose(fd);
}
fd = fopen("hello.txt", "r");
fgets(str, LENGTH, fd); /* 讀取文件內容 */
printf("%s"n", str);
fclose(fd);
}
4.小結
Linux提供的虛擬文件系統爲多種文件系統提供了統一的接口,Linux的文件編程有兩種途徑:基於Linux系統調用;基於C庫函數。這兩
種編程所涉及到文件操做有新建、打開、讀寫和關閉,對隨機文件還能夠定位。本章對這兩種編程方法都給出了具體的實例。
Linux下的C編程實戰(三)
――進程控制與進程通訊編程
1.Linux進程
Linux進程在內存中包含三部分數據:代碼段、堆棧段和數據段。代碼段存放了程序的代碼。代碼段能夠爲機器中運行同一程序的數個
進程共享。堆棧段存放的是子程序(函數)的返回地址、子程序的參數及程序的局部變量。而數據段則存放程序的全局變量、常數以及動態數
據分配的數據空間(好比用malloc函數申請的內存)。與代碼段不一樣,若是系統中同時運行多個相同的程序,它們不能使用同一堆棧段和數據
段。
Linux進程主要有以下幾種狀態:用戶狀態(進程在用戶狀態下運行的狀態)、內核狀態(進程在內核狀態下運行的狀態)、內存中就緒(進程
沒有執行,但處於就緒狀態,只要內核調度它,就能夠執行)、內存中睡眠(進程正在睡眠而且處於內存中,沒有被交換到SWAP設備)、就緒
且換出(進程處於就緒狀態,可是必須把它換入內存,內核才能再次調度它進行運行)、睡眠且換出(進程正在睡眠,且被換出內存)、被搶
先(進程從內核狀態返回用戶狀態時,內核搶先於它,作了上下文切換,調度了另外一個進程,原先這個進程就處於被搶先狀態)、建立狀態(
進程剛被建立,該進程存在,但既不是就緒狀態,也不是睡眠狀態,這個狀態是除了進程0之外的全部進程的最初狀態)、僵死狀態(進程調用
exit結束,進程再也不存在,但在進程表項中仍有記錄,該記錄可由父進程收集)。
下面咱們來以一個進程從建立到消亡的過程講解Linux進程狀態轉換的「生死因果」。
(1)進程被父進程經過系統調用fork建立而處於建立態;
(2)fork調用爲子進程配置好內核數據結構和子進程私有數據結構後,子進程進入就緒態(或者在內存中就緒,或者由於內存不夠而在SWAP設
備中就緒);
(3)若進程在內存中就緒,進程能夠被內核調度程序調度到CPU運行;
(4)內核調度該進程進入內核狀態,再由內核狀態返回用戶狀態執行。該進程在用戶狀態運行必定時間後,又會被調度程序所調度而進入內核
狀態,由此轉入就緒態。有時進程在用戶狀態運行時,也會由於須要內核服務,使用系統調用而進入內核狀態,服務完畢,會由內核狀態轉回
用戶狀態。要注意的是,進程在從內核狀態向用戶狀態返回時可能被搶佔,這是因爲有優先級更高的進程急需使用CPU,不能等到下一次調度時
機,從而形成搶佔;
(5)進程執行exit調用,進入僵死狀態,最終結束。
2.進程控制
進程控制中主要涉及到進程的建立、睡眠和退出等,在Linux中主要提供了fork、exec、clone的進程建立方法,sleep的進程睡眠和exit的進程
退出調用,另外Linux還提供了父進程等待子進程結束的系統調用wait。
fork
對於沒有接觸過Unix/Linux操做系統的人來講,fork是最難理解的概念之一,它執行一次卻返回兩個值,徹底「難以想象」。先看下面的程序
:
int main()
{
int i;
if (fork() == 0)
{
for (i = 1; i < 3; i++)
printf("This is child process"n");
}
else
{
for (i = 1; i < 3; i++)
printf("This is parent process"n");
}
}
執行結果爲:
This is child process
This is child process
This is parent process
This is parent process
fork在英文中是「分叉」的意思,這個名字取得很形象。一個進程在運行中,若是使用了fork,就產生了另外一個進程,因而進程就「分叉」了
。當前進程爲父進程,經過fork()會產生一個子進程。對於父進程,fork函數返回子程序的進程號而對於子程序,fork函數則返回零,這就是
一個函數返回兩次的本質。能夠說,fork函數是Unix系統最傑出的成就之一,它是七十年代Unix早期的開發者通過理論和實踐上的長期艱苦探
索後取得的成果。
若是咱們把上述程序中的循環放的大一點:
int main()
{
int i;
if (fork() == 0)
{
for (i = 1; i < 10000; i++)
printf("This is child process"n");
}
else
{
for (i = 1; i < 10000; i++)
printf("This is parent process"n");
}
};
則能夠明顯地看到父進程和子進程的併發執行,交替地輸出「This is child process」和「This is parent process」。
此時此刻,咱們尚未徹底理解fork()函數,再來看下面的一段程序,看看究竟會產生多少個進程,程序的輸出是什麼?
int main()
{
int i;
for (i = 0; i < 2; i++)
{
if (fork() == 0)
{
printf("This is child process"n");
}
else
{
printf("This is parent process"n");
}
}
};
exec
在Linux中可以使用exec函數族,包含多個函數(execl、execlp、execle、execv、execve和execvp),被用於啓動一個指定路徑和文件名的進程
。
exec函數族的特色體如今:某進程一旦調用了exec類函數,正在執行的程序就被幹掉了,系統把代碼段替換成新的程序(由exec類函數執行)
的代碼,而且原有的數據段和堆棧段也被廢棄,新的數據段與堆棧段被分配,可是進程號卻被保留。也就是說,exec執行的結果爲:系統認爲
正在執行的仍是原先的進程,可是進程對應的程序被替換了。
fork函數能夠建立一個子進程而當前進程不死,若是咱們在fork的子進程中調用exec函數族就能夠實現既讓父進程的代碼執行又啓動一個新的
指定進程,這實在是很妙的。fork和exec的搭配巧妙地解決了程序啓動另外一程序的執行但本身仍繼續運行的問題,請看下面的例子:
char command[MAX_CMD_LEN];
void main()
{
int rtn; /* 子進程的返回數值 */
while (1)
{
/* 從終端讀取要執行的命令 */
printf(">");
fgets(command, MAX_CMD_LEN, 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);
}
}
};
這個函數基本上實現了一個shell的功能,它讀取用戶輸入的進程名和參數,並啓動對應的進程。
clone
clone是Linux2.0之後才具有的新功能,它較fork更強(可認爲fork是clone要實現的一部分),可使得建立的子進程共享父進程的資源,並
且要使用此函數必須在編譯內核時設置clone_actually_works_ok選項。
clone函數的原型爲:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
此函數返回建立進程的PID,函數中的flags標誌用於設置建立子進程時的相關選項,具體含義以下表:
標誌
含義
CLONE_PARENT
建立的子進程的父進程是調用者的父進程,新進程與建立它的進程成了「兄弟」而不是「父子」
CLONE_FS
子進程與父進程共享相同的文件系統,包括root、當前目錄、umask
CLONE_FILES
子進程與父進程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS
在新的namespace啓動子進程,namespace描述了進程的文件hierarchy
CLONE_SIGHAND
子進程與父進程共享相同的信號處理(signal handler)表
CLONE_PTRACE
若父進程被trace,子進程也被trace
CLONE_VFORK
父進程被掛起,直至子進程釋放虛擬內存資源
CLONE_VM
子進程與父進程運行於相同的內存空間
CLONE_PID
子進程在建立時PID與父進程一致
CLONE_THREAD
Linux 2.4中增長以支持POSIX線程標準,子進程與父進程共享相同的線程羣
來看下面的例子:
int variable, fd;
int do_something() {
variable = 42;
close(fd);
_exit(0);
}
int main(int argc, char *argv[]) {
void **child_stack;
char tempch;
variable = 9;
fd = open("test.file", O_RDONLY);
child_stack = (void **) malloc(16384);
printf("The variable was %d"n", variable);
clone(do_something, child_stack, CLONE_VM|CLONE_FILES, NULL);
sleep(1); /* 延時以便子進程完成關閉文件操做、修改變量 */
printf("The variable is now %d"n", variable);
if (read(fd, &tempch, 1) < 1) {
perror("File Read Error");
exit(1);
}
printf("We could read from the file"n");
return 0;
}
運行輸出:
The variable is now 42
File Read Error
程序的輸出結果告訴咱們,子進程將文件關閉並將變量修改(調用clone時用到的CLONE_VM、CLONE_FILES標誌將使得變量和文件描述符表被共
享),父進程隨即就感受到了,這就是clone的特色。
sleep
函數調用sleep能夠用來使進程掛起指定的秒數,該函數的原型爲:
unsigned int sleep(unsigned int seconds);
該函數調用使得進程掛起一個指定的時間,若是指定掛起的時間到了,該調用返回0;若是該函數調用被信號所打斷,則返回剩餘掛起的時間數
(指定的時間減去已經掛起的時間)。
exit
系統調用exit的功能是終止本進程,其函數原型爲:
void _exit(int status);
_exit會當即終止發出調用的進程,全部屬於該進程的文件描述符都關閉。參數status做爲退出的狀態值返回父進程,在父進程中經過系統調用
wait可得到此值。
wait
wait系統調用包括:
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait的做用爲發出調用的進程只要有子進程,就睡眠到它們中的一個終止爲止; waitpid等待由參數pid指定的子進程退出。
3.進程間通訊
Linux的進程間通訊(IPC,InterProcess Communication)通訊方法有管道、消息隊列、共享內存、信號量、套接口等。
管道分爲有名管道和無名管道,無名管道只能用於親屬進程之間的通訊,而有名管道則可用於無親屬關係的進程之間。
#define INPUT 0
#define OUTPUT 1
void main()
{
int file_descriptors[2];
/*定義子進程號 */
pid_t pid;
char buf[BUFFER_LEN];
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);
}
}
上述程序中,無名管道以
int pipe(int filedis[2]);
方式定義,參數filedis返回兩個文件描述符filedes[0]爲讀而打開,filedes[1]爲寫而打開,filedes[1]的輸出是filedes[0]的輸入;
在Linux系統下,有名管道可由兩種方式建立(假設建立一個名爲「fifoexample」的有名管道):
(1)mkfifo("fifoexample","rw");
(2)mknod fifoexample p
mkfifo是一個函數,mknod是一個系統調用,即咱們能夠在shell下輸出上述命令。
有名管道建立後,咱們能夠像讀寫文件同樣讀寫之:
/* 進程一:讀有名管道*/
void main()
{
FILE *in_file;
int count = 1;
char buf[BUFFER_LEN];
in_file = fopen("pipeexample", "r");
if (in_file == NULL)
{
printf("Error in fdopen."n");
exit(1);
}
while ((count = fread(buf, 1, BUFFER_LEN, in_file)) > 0)
printf("received from pipe: %s"n", buf);
fclose(in_file);
}
/* 進程二:寫有名管道*/
void main()
{
FILE *out_file;
int count = 1;
char buf[BUFFER_LEN];
out_file = fopen("pipeexample", "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, BUFFER_LEN, out_file);
fclose(out_file);
}
消息隊列用於運行於同一臺機器上的進程間通訊,與管道類似;
共享內存一般由一個進程建立,其他進程對這塊內存區進行讀寫。獲得共享內存有兩種方式:映射/dev/mem設備和內存映像文件。前一種方式
不給系統帶來額外的開銷,但在現實中並不經常使用,由於它控制存取的是實際的物理內存;經常使用的方式是經過shmXXX函數族來實現共享內存:
int shmget(key_t key, int size, int flag); /* 得到一個共享存儲標識符 */
該函數使得系統分配size大小的內存用做共享內存;
void *shmat(int shmid, void *addr, int flag); /* 將共享內存鏈接到自身地址空間中*/
shmid爲shmget函數返回的共享存儲標識符,addr和flag參數決定了以什麼方式來肯定鏈接的地址,函數的返回值便是該進程數據段所鏈接的實
際地址。此後,進程能夠對此地址進行讀寫操做訪問共享內存。
本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取情況。通常說來,爲了得到共享資源,進程須要執行下列操做:
(1)測試控制該資源的信號量;
(2)若此信號量的值爲正,則容許進行使用該資源,進程將進號量減1;
(3)若此信號量爲0,則該資源目前不可用,進程進入睡眠狀態,直至信號量值大於0,進程被喚醒,轉入步驟(1);
(4)當進程再也不使用一個信號量控制的資源時,信號量值加1,若是此時有進程正在睡眠等待此信號量,則喚醒此進程。
下面是一個使用信號量的例子,該程序建立一個特定的IPC結構的關鍵字和一個信號量,創建此信號量的索引,修改索引指向的信號量的值,最
後清除信號量:
#include
#include
#include
#include
void main()
{
key_t unique_key; /* 定義一個IPC關鍵字*/
int id;
struct sembuf lock_it;
union semun options;
int i;
unique_key = ftok(".", 'a'); /* 生成關鍵字,字符'a'是一個隨機種子*/
/* 建立一個新的信號量集合*/
id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);
printf("semaphore id=%d"n", id);
options.val = 1; /*設置變量值*/
semctl(id, 0, SETVAL, options); /*設置索引0的信號量*/
/*打印出信號量的值*/
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d"n", i);
/*下面從新設置信號量*/
lock_it.sem_num = 0; /*設置哪一個信號量*/
lock_it.sem_op = - 1; /*定義操做*/
lock_it.sem_flg = IPC_NOWAIT; /*操做方式*/
if (semop(id, &lock_it, 1) == - 1)
{
printf("can not lock semaphore."n");
exit(1);
}
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d"n", i);
/*清除信號量*/
semctl(id, 0, IPC_RMID, 0);
}
套接字通訊並不爲Linux所專有,在全部提供了TCP/IP協議棧的操做系統中幾乎都提供了socket,而全部這樣操做系統,對套接字的編程方法幾
乎是徹底同樣的。
4.小節
本章講述了Linux進程的概念,並以多個實例講解了進程控制及進程間通訊方法,理解這一章的內容能夠說是理解Linux這個操做系統的關鍵。
Linux下的C編程實戰(四)
――「線程」控制與「線程」通訊編程
1.Linux「線程」
筆者曾經在《基於嵌入式操做系統VxWorks的多任務併發程序設計》(《軟件報》2006年第5~12期)中詳細敘述了進程和線程的區別,
並曾經說明Linux是一種「多進程單線程」的操做系統。Linux自己只有進程的概念,而其所謂的「線程」本質上在內核裏仍然是進程。你們知
道,進程是資源分配的單位,同一進程中的多個線程共享該進程的資源(如做爲共享內存的全局變量)。Linux中所謂的「線程」只是在被建立
的時候「克隆」(clone)了父進程的資源,所以,clone出來的進程表現爲「線程」,這一點必定要弄清楚。所以,Linux「線程」這個概念只有
在打冒號的狀況下才是最準確的,惋惜的是幾乎沒有書籍留心去強調這一點。
Linux內核只提供了輕量進程的支持,未實現線程模型,但Linux盡最大努力優化了進程的調度開銷,這在必定程度上彌補無線程的缺陷
。Linux用一個核心進程(輕量進程)對應一個線程,將線程調度等同於進程調度,交給核心完成。
目前Linux中最流行的線程機制爲LinuxThreads,所採用的就是線程-進程「一對一」模型,調度交給核心,而在用戶級實現一個包括信號處理
在內的線程管理機制。LinuxThreads由Xavier Leroy (Xavier.Leroy@inria.fr)負責開發完成,並已綁定在GLIBC中發行,它實現了一種
BiCapitalized面向Linux的Posix 1003.1c 「pthread」標準接口。Linuxthread能夠支持Intel、Alpha、MIPS等平臺上的多處理器系統。
按照POSIX 1003.1c 標準編寫的程序與Linuxthread 庫相連接便可支持Linux平臺上的多線程,在程序中需包含頭文件pthread. h,在編譯連接
時使用命令:
gcc -D -REENTRANT -lpthread xxx. c
其中-REENTRANT宏使得相關庫函數(如stdio.h、errno.h中函數) 是可重入的、線程安全的(thread-safe),-lpthread則意味着連接庫目錄下的
libpthread.a或libpthread.so文件。使用Linuxthread庫須要2.0以上版本的Linux內核及相應版本的C庫(libc 5.2.1八、libc 5.4.十二、libc 6)
。
2.「線程」控制
線程建立
進程被建立時,系統會爲其建立一個主線程,而要在進程中建立新的線程,則能夠調用pthread_create:
pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(start_routine)(void*), void *arg);
start_routine爲新線程的入口函數,arg爲傳遞給start_routine的參數。
每一個線程都有本身的線程ID,以便在進程內區分。線程ID在pthread_create調用時回返給建立線程的調用者;一個線程也能夠在建立後使用
pthread_self()調用獲取本身的線程ID:
pthread_self (void) ;
線程退出
線程的退出方式有三:
(1)執行完成後隱式退出;
(2)由線程自己顯示調用pthread_exit 函數退出;
pthread_exit (void * retval) ;
(3)被其餘線程用pthread_cance函數終止:
pthread_cance (pthread_t thread) ;
在某線程中調用此函數,能夠終止由參數thread 指定的線程。
若是一個線程要等待另外一個線程的終止,可使用pthread_join函數,該函數的做用是調用pthread_join的線程將被掛起直到線程ID爲參數
thread的線程終止:
pthread_join (pthread_t thread, void** threadreturn);
3.線程通訊
線程互斥
互斥意味着「排它」,即兩個線程不能同時進入被互斥保護的代碼。Linux下能夠經過pthread_mutex_t 定義互斥體機制完成多線程的互斥操做
,該機制的做用是對某個須要互斥的部分,在進入時先獲得互斥體,若是沒有獲得互斥體,代表互斥部分被其它線程擁有,此時欲獲取互斥體
的線程阻塞,直到擁有該互斥體的線程完成互斥部分的操做爲止。
下面的代碼實現了對共享全局變量x 用互斥體mutex 進行保護的目的:
int x; // 進程中的全局變量
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); //按缺省的屬性初始化互斥體變量mutex
pthread_mutex_lock(&mutex); // 給互斥體變量加鎖
… //對變量x 的操做
phtread_mutex_unlock(&mutex); // 給互斥體變量解除鎖
線程同步
同步就是線程等待某個事件的發生。只有當等待的事件發生線程才繼續執行,不然線程掛起並放棄處理器。當多個線程協做時,相互做用的任
務必須在必定的條件下同步。
Linux下的C語言編程有多種線程同步機制,最典型的是條件變量(condition variable)。pthread_cond_init用來建立一個條件變量,其函數原
型爲:
pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *attr);
pthread_cond_wait和pthread_cond_timedwait用來等待條件變量被設置,值得注意的是這兩個等待調用須要一個已經上鎖的互斥體mutex,這
是爲了防止在真正進入等待狀態以前別的線程有可能設置該條件變量而產生競爭。pthread_cond_wait的函數原型爲:
pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_broadcast用於設置條件變量,即便得事件發生,這樣等待該事件的線程將再也不阻塞:
pthread_cond_broadcast (pthread_cond_t *cond) ;
pthread_cond_signal則用於解除某一個等待線程的阻塞狀態:
pthread_cond_signal (pthread_cond_t *cond) ;
pthread_cond_destroy 則用於釋放一個條件變量的資源。
在頭文件semaphore.h 中定義的信號量則完成了互斥體和條件變量的封裝,按照多線程程序設計中訪問控制機制,控制對資源的同步訪問,提
供程序設計人員更方便的調用接口。
sem_init(sem_t *sem, int pshared, unsigned int val);
這個函數初始化一個信號量sem 的值爲val,參數pshared 是共享屬性控制,代表是否在進程間共享。
sem_wait(sem_t *sem);
調用該函數時,若sem爲無狀態,調用線程阻塞,等待信號量sem值增長(post )成爲有信號狀態;若sem爲有狀態,調用線程順序執行,但信號
量的值減一。
sem_post(sem_t *sem);
調用該函數,信號量sem的值增長,能夠從無信號狀態變爲有信號狀態。
4.實例
下面咱們仍是以著名的生產者/消費者問題爲例來闡述Linux線程的控制和通訊。一組生產者線程與一組消費者線程經過緩衝區發生聯繫。生產
者線程將生產的產品送入緩衝區,消費者線程則從中取出產品。緩衝區有N 個,是一個環形的緩衝池。
#include
#include
#define BUFFER_SIZE 16 // 緩衝區數量
struct prodcons
{
// 緩衝區相關數據結構
int buffer[BUFFER_SIZE]; /* 實際數據存放的數組*/
pthread_mutex_t lock; /* 互斥體lock 用於對緩衝區的互斥操做 */
int readpos, writepos; /* 讀寫指針*/
pthread_cond_t notempty; /* 緩衝區非空的條件變量 */
pthread_cond_t notfull; /* 緩衝區未滿的條件變量 */
};
/* 初始化緩衝區結構 */
void init(struct prodcons *b)
{
pthread_mutex_init(&b->lock, NULL);
pthread_cond_init(&b->notempty, NULL);
pthread_cond_init(&b->notfull, NULL);
b->readpos = 0;
b->writepos = 0;
}
/* 將產品放入緩衝區,這裏是存入一個整數*/
void put(struct prodcons *b, int data)
{
pthread_mutex_lock(&b->lock);
/* 等待緩衝區未滿*/
if ((b->writepos + 1) % BUFFER_SIZE == b->readpos)
{
pthread_cond_wait(&b->notfull, &b->lock);
}
/* 寫數據,並移動指針 */
b->buffer[b->writepos] = data;
b->writepos++;
if (b->writepos > = BUFFER_SIZE)
b->writepos = 0;
/* 設置緩衝區非空的條件變量*/
pthread_cond_signal(&b->notempty);
pthread_mutex_unlock(&b->lock);
}
/* 從緩衝區中取出整數*/
int get(struct prodcons *b)
{
int data;
pthread_mutex_lock(&b->lock);
/* 等待緩衝區非空*/
if (b->writepos == b->readpos)
{
pthread_cond_wait(&b->notempty, &b->lock);
}
/* 讀數據,移動讀指針*/
data = b->buffer[b->readpos];
b->readpos++;
if (b->readpos > = BUFFER_SIZE)
b->readpos = 0;
/* 設置緩衝區未滿的條件變量*/
pthread_cond_signal(&b->notfull);
pthread_mutex_unlock(&b->lock);
return data;
}
/* 測試:生產者線程將1 到10000 的整數送入緩衝區,消費者線
程從緩衝區中獲取整數,二者都打印信息*/
#define OVER ( - 1)
struct prodcons buffer;
void *producer(void *data)
{
int n;
for (n = 0; n < 10000; n++)
{
printf("%d --->"n", n);
put(&buffer, n);
} put(&buffer, OVER);
return NULL;
}
void *consumer(void *data)
{
int d;
while (1)
{
d = get(&buffer);
if (d == OVER)
break;
printf("--->%d "n", d);
}
return NULL;
}
int main(void)
{
pthread_t th_a, th_b;
void *retval;
init(&buffer);
/* 建立生產者和消費者線程*/
pthread_create(&th_a, NULL, producer, 0);
pthread_create(&th_b, NULL, consumer, 0);
/* 等待兩個線程結束*/
pthread_join(th_a, &retval);
pthread_join(th_b, &retval);
return 0;
}
5.WIN3二、VxWorks、Linux線程類比
目前爲止,筆者已經創做了《基於嵌入式操做系統VxWorks的多任務併發程序設計》(《軟件報》2006年5~12期連載)、《深刻淺出Win32多線
程程序設計》(天極網技術專題)系列,咱們來找出這兩個系列文章與本文的共通點。
看待技術問題要瞄準其本質,無論是Linux、VxWorks仍是WIN32,其涉及到多線程的部分都是那些內容,無非就是線程控制和線程通訊,它們的
許多函數只是名稱不一樣,其實質含義是等價的,下面咱們來列個三大操做系統共同點詳細表單:
事項
WIN32
VxWorks
Linux
線程建立
CreateThread
taskSpawn
pthread_create
線程終止
執行完成後退出;線程自身調用ExitThread 函數即終止本身;被其餘線程調用函數TerminateThread函數
執行完成後退出;由線程自己調用exit退出;被其餘線程調用函數taskDelete終止
執行完成後退出;由線程自己調用pthread_exit 退出;被其餘線程調用函數pthread_cance終止
獲取線程ID
GetCurrentThreadId
taskIdSelf
pthread_self
建立互斥
CreateMutex
semMCreate
pthread_mutex_init
獲取互斥
WaitForSingleObject、
WaitForMultipleObjects
semTake
pthread_mutex_lock
釋放互斥
ReleaseMutex
semGive
phtread_mutex_unlock
建立信號量
CreateSemaphore
semBCreate、semCCreate
sem_init
等待信號量
WaitForSingleObject
semTake
sem_wait
釋放信號量
ReleaseSemaphore
semGive
sem_post
6.小結
本章講述了Linux下多線程的控制及線程間通訊編程方法,給出了一個生產者/消費者的實例,並將Linux的多線程與WIN3二、VxWorks多
線程進行了類比,總結了通常規律。鑑於多線程編程已成爲開發併發應用程序的主流方法,學好本章的意義也便不言自明。
Linux下的C編程實戰(五)
――驅動程序設計
1.引言
設備驅動程序是操做系統內核和機器硬件之間的接口,它爲應用程序屏蔽硬件的細節,通常來講,Linux的設備驅動程序須要完成以下功能:
(1)初始化設備;
(2)提供各種設備服務;
(3)負責內核和設備之間的數據交換;
(4)檢測和處理設備工做過程當中出現的錯誤。
妙趣橫生的是,Linux下的設備驅動程序被組織爲一組完成不一樣任務的函數的集合,經過這些函數使得Windows的設備操做猶如文件通常。在應
用程序看來,硬件設備只是一個設備文件,應用程序能夠象操做普通文件同樣對硬件設備進行操做。本系列文章的第2章文件系統編程中已經看
到了這些函數的真面目,它們就是open ()、close ()、read ()、write () 等。
Linux主要將設備分爲二類:字符設備和塊設備(固然網絡設備及USB等其它設備的驅動編寫方法又稍有不一樣)。這兩類設備的不一樣點在於:在
對字符設備發出讀/寫請求時,實際的硬件I/O通常就緊接着發生了,而塊設備則否則,它利用一塊系統內存做緩衝區,當用戶進程對設備請求
能知足用戶的要求,就返回請求的數據,若是不能,就調用請求函數來進行實際的I/O操做。塊設備主要針對磁盤等慢速設備。以字符設備的驅
動較爲簡單,所以本章主要闡述字符設備的驅動編寫。
2.驅動模塊函數
init 函數用來完成對所控設備的初始化工做,並調用register_chrdev() 函數註冊字符設備。假設有一字符設備「exampledev」,則其init
函數爲:
void exampledev_init(void)
{
if (register_chrdev(MAJOR_NUM, " exampledev ", &exampledev_fops))
TRACE_TXT("Device exampledev driver registered error");
else
TRACE_TXT("Device exampledev driver registered successfully");
…//設備初始化
}
其中,register_chrdev函數中的參數MAJOR_NUM爲主設備號,「exampledev」爲設備名,exampledev_fops爲包含基本函數入口點的結構體,類
型爲file_operations。當執行exampledev_init時,它將調用內核函數register_chrdev,把驅動程序的基本入口點指針存放在內核的字符設備
地址表中,在用戶進程對該設備執行系統調用時提供入口地址。
file_operations結構體定義爲:
struct file_operations
{
int (*lseek)();
int (*read)();
int (*write)();
int (*readdir)();
int (*select)();
int (*ioctl)();
int (*mmap)();
int (*open)();
void(*release)();
int (*fsync)();
int (*fasync)();
int (*check_media_change)();
void(*revalidate)();
};
大多數的驅動程序只是利用了其中的一部分,對於驅動程序中無需提供的功能,只須要把相應位置的值設爲NULL。對於字符設備來講,要提供
的主要入口有:open ()、release ()、read ()、write ()、ioctl ()。
open()函數 對設備特殊文件進行open()系統調用時,將調用驅動程序的open () 函數:
int open(struct inode * inode ,struct file * file);
其中參數inode爲設備特殊文件的inode (索引結點) 結構的指針,參數file是指向這一設備的文件結構的指針。open()的主要任務是肯定硬件
處在就緒狀態、驗證次設備號的合法性(次設備號能夠用MINOR(inode-> i - rdev) 取得)、控制使用設備的進程數、根據執行狀況返回狀態碼
(0表示成功,負數表示存在錯誤) 等;
release()函數 當最後一個打開設備的用戶進程執行close ()系統調用時,內核將調用驅動程序的release () 函數:
void release (struct inode * inode ,struct file * file) ;
release 函數的主要任務是清理未結束的輸入/輸出操做、釋放資源、用戶自定義排他標誌的復位等。
read()函數 當對設備特殊文件進行read() 系統調用時,將調用驅動程序read() 函數:
void read(struct inode * inode ,struct file * file ,char * buf ,int count) ;
參數buf是指向用戶空間緩衝區的指針,由用戶進程給出,count 爲用戶進程要求讀取的字節數,也由用戶給出。
read() 函數的功能就是從硬設備或內核內存中讀取或複製count個字節到buf 指定的緩衝區中。在複製數據時要注意,驅動程序運行在內核中
,而buf指定的緩衝區在用戶內存區中,是不能直接在內核中訪問使用的,所以,必須使用特殊的複製函數來完成複製工做,這些函數在
segment.h>中定義:
void put_user_byte (char data_byte ,char * u_addr) ;
void put_user_word (short data_word ,short * u_addr) ;
void put_user_long(long data_long ,long * u_addr) ;
void memcpy_tofs (void * u_addr ,void * k_addr ,unsigned long cnt) ;
參數u_addr爲用戶空間地址,k_addr 爲內核空間地址,cnt爲字節數。
write( ) 函數 當設備特殊文件進行write () 系統調用時,將調用驅動程序的write () 函數:
void write (struct inode * inode ,struct file * file ,char * buf ,int count) ;
write ()的功能是將參數buf 指定的緩衝區中的count 個字節內容複製到硬件或內核內存中,和read() 同樣,複製工做也須要由特殊函數來完
成:
unsigned char_get_user_byte (char * u_addr) ;
unsigned char_get_user_word (short * u_addr) ;
unsigned char_get_user_long(long * u_addr) ;
unsigned memcpy_fromfs(void * k_addr ,void * u_addr ,unsigned long cnt) ;
ioctl() 函數 該函數是特殊的控制函數,能夠經過它向設備傳遞控制信息或從設備取得狀態信息,函數原型爲:
int ioctl (struct inode * inode ,struct file * file ,unsigned int cmd ,unsigned long arg);
參數cmd爲設備驅動程序要執行的命令的代碼,由用戶自定義,參數arg 爲相應的命令提供參數,類型能夠是整型、指針等。
一樣,在驅動程序中,這些函數的定義也必須符合命名規則,按照本文約定,設備「exampledev」的驅動程序的這些函數應分別命名爲
exampledev_open、exampledev_ release、exampledev_read、exampledev_write、exampledev_ioctl,所以設備「exampledev」的基本入口點
結構變量exampledev_fops 賦值以下:
struct file_operations exampledev_fops {
NULL ,
exampledev_read ,
exampledev_write ,
NULL ,
NULL ,
exampledev_ioctl ,
NULL ,
exampledev_open ,
exampledev_release ,
NULL ,
NULL ,
NULL ,
NULL
} ;
3.內存分配
因爲Linux驅動程序在內核中運行,所以在設備驅動程序須要申請/釋放內存時,不能使用用戶級的malloc/free函數,而需由內核級的函數
kmalloc/kfree () 來實現,kmalloc()函數的原型爲:
void kmalloc (size_t size ,int priority);
參數size爲申請分配內存的字節數;參數priority說明若kmalloc()不能立刻分配內存時用戶進程要採用的動做:GFP_KERNEL 表示等待,即等
kmalloc()函數將一些內存安排到交換區來知足你的內存須要,GFP_ATOMIC 表示不等待,如不能當即分配到內存則返回0 值;函數的返回值指
向已分配內存的起始地址,出錯時,返回0。
kmalloc ()分配的內存需用kfree()函數來釋放,kfree ()被定義爲:
# define kfree (n) kfree_s( (n) ,0)
其中kfree_s () 函數原型爲:
void kfree_s (void * ptr ,int size);
參數ptr爲kmalloc()返回的已分配內存的指針,size是要釋放內存的字節數,若爲0 時,由內核自動肯定內存的大小。
4.中斷
許多設備涉及到中斷操做,所以,在這樣的設備的驅動程序中須要對硬件產生的中斷請求提供中斷服務程序。與註冊基本入口點同樣,驅動程
序也要請求內核將特定的中斷請求和中斷服務程序聯繫在一塊兒。在Linux中,用request_irq()函數來實現請求:
int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);
參數irq爲要中斷請求號,參數handler爲指向中斷服務程序的指針,參數type 用來肯定是正常中斷仍是快速中斷(正常中斷指中斷服務子程序
返回後,內核能夠執行調度程序來肯定將運行哪個進程;而快速中斷是指中斷服務子程序返回後,當即執行被中斷程序,正常中斷type 取值
爲0 ,快速中斷type 取值爲SA_INTERRUPT),參數name是設備驅動程序的名稱。
5.實例
筆者最近設計了一塊採用三星S3C2410 ARM處理器的電路板(ARM處理器普遍應用於手機、PDA等嵌入式系統),板上包含四個用戶可編程的發光
二極管(LED),這些LED鏈接在ARM處理器的可編程I/O口(GPIO)上。下圖給出了ARM中央處理器與LED的鏈接原理:
咱們在ARM處理器上移植Linux操做系統,如今來編寫這些LED的驅動:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "leds" /*定義led 設備的名字*/
#define LED_MAJOR 231 /*定義led 設備的主設備號*/
static unsigned long led_table[] =
{
/*I/O 方式led 設備對應的硬件資源*/
GPIO_B10, GPIO_B8, GPIO_B5, GPIO_B6,
};
/*使用ioctl 控制led*/
static int leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd,
unsigned long arg)
{
switch (cmd)
{
case 0:
case 1:
if (arg > 4)
{
return -EINVAL;
}
write_gpio_bit(led_table[arg], !cmd);
default:
return -EINVAL;
}
}
static struct file_operations leds_fops =
{
owner: THIS_MODULE, ioctl: leds_ioctl,
};
static devfs_handle_t devfs_handle;
static int __init leds_init(void)
{
int ret;
int i;
/*在內核中註冊設備*/
ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);
if (ret < 0)
{
printk(DEVICE_NAME " can't register major number"n");
return ret;
}
devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, LED_MAJOR,
0, S_IFCHR | S_IRUSR | S_IWUSR, &leds_fops, NULL);
/*使用宏進行端口初始化,set_gpio_ctrl 和write_gpio_bit 均爲宏定義*/
for (i = 0; i < 8; i++)
{
set_gpio_ctrl(led_table[i] | GPIO_PULLUP_EN | GPIO_MODE_OUT);
write_gpio_bit(led_table[i], 1);
}
printk(DEVICE_NAME " initialized"n");
return 0;
}
static void __exit leds_exit(void)
{
devfs_unregister(devfs_handle);
unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}
module_init(leds_init);
module_exit(leds_exit);
使用命令方式編譯led 驅動模塊:
#arm-linux-gcc -D__KERNEL__ -I/arm/kernel/include
-DKBUILD_BASENAME=leds -DMODULE -c -o leds.o leds.c
以上命令將生成leds.o 文件,把該文件複製到板子的/lib目錄下,使用如下命令就能夠安裝leds驅動模塊:
#insmod /lib/ leds.o
刪除該模塊的命令是:
#rmmod leds
6.小結
本章講述了Linux設備驅動程序的入口函數及驅動程序中的內存申請、中斷等,並給出了一個經過ARM處理器的GPIO口控制LED的驅動實例。
posted on 2008-05-21 14:07 exce4 閱讀(352) 評論(1) 編輯 收藏 網摘
評論 #1樓
2008-06-04 09:14 移動代理 [未註冊用戶]陳工程師一直作Linux的嵌入式開發,做爲在開發一線的工程師,他對不少問題的見解可能更切合實際需求,因而,經過電子郵件,就嵌入式開發方面的問題,請他談了一下本身的見解: 問:關於嵌入式開發,咱們準備給同窗們講解一些入門知識,從你一線開發經驗來講,給咱們一些建議: 陳工回答: 對於嵌入式Linux入門,若是有必定基礎,能夠從驅動開始;若是沒有基礎,我我的建議仍是從應用程序開始。由於從應用程序開始是最容易的,也是最直觀 的。而驅動程序運行在內核態,驅動自己的結構就比較複雜,若是要完全弄明白驅動的運行機制,一定牽涉內核,對於高年級的學生恐怕問題會少一些,而 對於低年級的學生,問題估計較多。我曾經遇到過一些初學者,就是一入門就栽了,失去了信心,固然這只是少數。不過,若是在遇到問題以後,可以獲得即時、 正確的點化,那就是好事了。 既然您決定講驅動,那就從內核模塊開始。在PC上就能夠進行的虛擬設備實驗,如基於內存的內核模塊。能夠考慮從模塊的結構、編譯、插入、卸載等方 面進行闡述。 驅動模塊無非分字符驅動、塊設備驅動和網絡驅動三大類。可是必定要讓學生知道,任何一個系統,特別是嵌入式系統,而且在目前的嵌入式Linux產品 開發中,最簡單、最重要、最多、最複雜的也是字符設備驅動,從IO驅動到串口驅動、到USB驅動等等,廣義上都是字符驅動。讓學生最好專一於字符設備驅 動,由於一個嵌入式設備,網卡通常一塊,FLASH通常也是一塊(也包括幾塊組成的FLASH組),可是這兩方面,基本都有完善的驅動,如網卡驅動有很 多,塊設備驅動,硬件層已經有通用接口,無論是NOR FLASH仍是NAND FLASH,文件系統層更是有了很是多、很是成熟的文件系統,如 JFFS二、YAFFS、YAFFS二、EXT二、EXT三、ROMFS、CRAMFS等等,無需咱們再去研究,學會應用便可。而除此以外的其它設備, 如AD、DA、CAN、RS485等等,都是須要根據應用來進行設計的,這纔是一個產品區別於其它產品的重點,更是市場價值增值點。 另外呢,也是前一點引伸爲而來的,學習Linux,準備作產品的話,不要把Linux當成了終極目標(固然,這是對應用而言的),要有隻是把 Linux當成一個平臺的思想。更重要的還在各類產品所需求的專業技術,如通訊方面像CAN、RS48五、GPRS等等,或者工業控制方面,IO控制、 實時特性等等。Linxu博大精深,研究起來永無止境,可是在產品中,只要到了一個產品夠用就能夠了(固然,多一些更好,要視人而定)。 問:嵌入式應用程序的開發,應用場景較多的是圖形界面仍是字符界面,若是是圖形界面,開發環境QT和Minigui哪種更合適,哪一種類型的應用程序在嵌入式系統中應用比較多? 陳工回答: 對於嵌入式Linux的應用,大多數的應用並不須要圖形界面,好比交換機、路由器、嵌入式網關以及服務器等等。圖形界面呢,主要應用在多媒體、手機等手持設備和一些須要圖形界面的人機交互系統。 嵌入式Linux可選圖形界面不少,上網找找的話,能夠發現遠非咱們常說的QT、MiniGUI等。包括Tiny-X,matchbox、 OPIE、GPE等等。不一樣GUI有本身的特點,有本身的特殊應用場合,對於產品開發,根據須要選擇合適的GUI。對於學習,天然是選擇容易獲得、容易開 發的GUI。QT是一個不錯的選擇,因爲QT有一個PC上的模擬器,能夠在沒有實際液晶LCD的狀況下,甚至在沒有任何硬件的狀況下均可以在PC上進行模 擬開發。QT是收費的,固然,有免費版可用。MiniGUI呢,純粹國產的,支持國貨,能夠考慮選擇MiniGUI。這是一個輕量級的嵌入式GUI,能夠 跨平臺,學習版也才100多塊。MiniGUI能夠用於工業控制場合,QT在這方面的應用目前尚未遇到,主要用在手持設備。 咱們在開發中採用Tiny-X,這也是一個能夠用於工業控制的GUI,基本兼容X-Window,體積小,佔用資源少,速度快,穩定。 對於Linux的應用程序開發,除了GUI程序以外,最基本的應用程序有: (1)串口編程。不管是在Windows下仍是Linux下,串口編程都是極爲複雜的,可是很是鍛鍊一我的的編程水平和能力。 (2)網絡編程以及WEB相關編程。網絡編程的tcp、udp、tcp/ip等。至於WEB編程,主要是在系統開啓一個WEB服務器,製做一些網頁,經過遠程登陸可以對整個系統進行配置甚至升級等功能。好比咱們的路由器配置網頁。這種應用在之後會愈來愈普遍。 (3)另一個就是Shell編程了。Shell的做用我想,*NIX世界的人都很清楚。在不少應用裏面,經過一些很是富有技巧性的Shell腳本,實現了很是複雜的功能,包括遠程系統升級等。 以上我提到的這3方面,很是易於實驗,在沒有硬件,只有PC的狀況均可以作。 學生電腦安裝ubuntu,那之後配置嵌入式Linux開發環境可能遇到的問題會多一點。不過不要緊,可以解決的。在我我的看來ubuntu適合於家用、 辦公,但要用於開發,配置難度稍微大一點。不過沒有辦法,如今電腦硬件太新,最適合的RedHat 9.0沒法安裝。