Linux下的C編程實戰

来源:http://www.cnblogs.com/roucheng/archive/2016/05/31/linuxc.html
-Advertisement-
Play Games

Linux下的C編程實戰(一) ――開發平臺搭建 1.引言 Linux操作系統在伺服器領域的應用和普及已經有較長的歷史,這源於它的開源特點以及其超越Windows的安全性和穩定性。而近年來, Linux操作系統在嵌入式系統領域的延伸也可謂是如日中天,許多版本的嵌入式Linux系統被開發出來,如ucL ...


Linux下的C編程實戰(一)

――開發平臺搭建


1.引言

       Linux操作系統在伺服器領域的應用和普及已經有較長的歷史,這源於它的開源特點以及其超越Windows的安全性和穩定性。而近年來,

Linux操作系統在嵌入式系統領域的延伸也可謂是如日中天,許多版本的嵌入式Linux系統被開發出來,如ucLinux、RTLinux、ARM-Linux等等。

在嵌入式操作系統方面,Linux的地位是不容懷疑的,它開源、它包含TCP/IP協議棧、它易集成GUI。

       鑒於Linux操作系統在伺服器和嵌入式系統領域愈來愈廣泛的應用,社會上越來越需要基於Linux操作系統進行編程的開發人員。

瀏覽許多論壇,經常碰到這樣的提問:“現在是不是很流行unix/linux下的c編程?所以想學習一下!但是不知道該從何學起,如何下手!有什

麽好的建議嗎?各位高手!哪些書籍比較合適初學者?在深入淺出的過程中應該看哪些不同層次的書?比如好的網站、論壇請大家賜教!不慎

感激!”

鑒於讀者的需求,在本文中,筆者將對Linux平臺下C編程的幾個方面進行實例講解,並力求回答讀者們關心的問題,以與讀者朋友們進行交流

,共同提高。在本文的連載過程中,有任何問題或建議,您也可以進入筆者的博客參與討論:

http://hovertree.com/

筆者建議在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");

}/* 何問起 hovertree.com */

最簡單的編譯方法是不指定任何編譯選項:

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>,它在進入指定函數(function)時停住。

相反的,clear用於清除所有的已定義的斷點,clear <function>清除設置在函數上的斷點,  clear <linenum>則清除設置在指定行上的斷點

watch命令:

watch命令用於觀查變數或表達式的值,我們觀查sum變數只需要運行watch sum:


watch <expr>為表達式(變數)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、ext2、minix、iso9660、msdos、fat、vfat、nfs等。在這些具體文件系統的上層,Linux提供了虛擬

文件系統(VFS)來統一它們的行為,虛擬文件系統為不同的文件系統與內核的通信提供了一致的介面。下圖給出了Linux中文件系統的關係:

<!--[if !vml]--><!--[endif]-->

       在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 <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdio.h>

#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 <stdio.h>

#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 <stdio.h>

#include <sys/types.h>

#include <sys/sem.h>

#include <sys/ipc.h>

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 ([email protected])負責開發完成,並已綁定在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.18、libc 5.4.12、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 <stdio.h>

#include <pthread.h>

#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.WIN32、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的多線程與WIN32、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指定的緩衝區在用戶記憶體區中,是不能直接在內核中訪問使用的,因此,必須使用特殊的複製函數來完成複製工作,這些函數在<asm/

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”的基本入口點

結構變數example

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用 mysqladmin 創建資料庫 使用普通用戶,你可能需要特定的許可權來創建或者刪除 MySQL 資料庫。 所以我們這邊使用root用戶登錄,root用戶擁有最高許可權,可以使用 mysql mysqladmin 命令來創建資料庫。 實例 以下命令簡單的演示了創建資料庫的過程,數據名為 TUTOR ...
  • db.集合名稱.update({query},{update},upsert, multi})query:過濾條件update:修改內容upsert:如果不存在查詢條件查出的記錄,是否插入一條數據,預設是falsemulti:是否只修改查詢條件查出的第一條記錄,預設是false 把整條數據都修改了, ...
  • 訪問FTP站點下載文件,提示“當前的安全設置不允許從該位置下載文件”的解決方案: ...
  • 最近用linux在玩Tomcat,啟動的時候總是會報錯(8080/8009/8005) 於是整理了一下網上零亂的查看PID和埠的命令,以備記錄。 1.由埠號查詢PID號 首先myeclipse報錯的時候會提示:“8009埠被占用”,那麼你不得不依據此埠去查看該埠下運行的哪些進程 使用命令來 ...
  • 作為收購 NeXT 公司的結果,蘋果公司獲得了 NeXTSTEP 架構中的 Mach 和 Objective-C 等設計。儘管 NeXTSTEP 本身已經不再發展了,但是其中的組件在 OS X 中獲得了新生。事實上,可以將 OS X 看成是 Mac OS Classic 和NeXTSTEP 的融合, ...
  • 操作系統:CentOS6.6_32位 控制腳本目錄/etc/rc.d,該目錄下存在各個運行級別的腳本文件,執行ls /etc/rc.d,顯示結果為:init.d rc rc0.d rc1.d rc2.d rc3.d rc4.d rc5.d rc6.d rc.local rc.sysinit。 /et ...
  • 當我們想操控一個硬體的時候,我們有必要先去瞭解這個硬體的一些物理特性,比如如何點亮LED,那麼我們首先就得瞭解LED的一些特性,如下: LED本身有兩個接線點,一個是LED的負極,一個是LED的正極。LED這個硬體本身存在的作用就是亮或者不亮,而我們想要LED亮或者不亮,那就可以通過對LED的正負極 ...
  • Samba是在Linux和UNIX系統上實現SMB協議的一個免費軟體,由伺服器及客戶端程式構成。SMB(Server Messages Block,信息服務塊)是一種在區域網上共用文件和印表機的一種通信協議,它為區域網內的不同電腦之間提供文件及印表機等資源的共用服務。 環境:Win7_64位+VM ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...