1. 程式人生 > >嵌入式高階程式設計

嵌入式高階程式設計

1.1 彙編程式的Hello world .data msg:                      #首地址     .ascii "Hello, World!\n"     len = . - msg .text     .global _start        #彙編程式的格式 _start:     movl $len, %edx     movl $msg, %ecx     movl $1, %ebx     movl $4, %eax     int $0x80     movl $0, %ebx     movl $1, %eax     int $0x80 執行效果與命令如下

這段組合語言相當於以下C程式碼: #include <unistd.h> char msg[14] = "Hello, world\n"; #define len 14 int main(void) {     write(1, msg, len);     _exit(0); }      .data段有一個編號msg,代表字串“Hello,world!\n”的首地址,相當於C程式的一個全域性變數。注意在C語言中字串的末尾隱含有一個'\0',而彙編指示.ascii定義 的字串末尾沒有隱含的'\0'。彙編程式中的len代表一個常量,它的值由當前的地址減去符號msg所代表的地址得到,換句話說就是字串“Hello,world!\n”的長度。 現在解釋一下這行程式碼中.,彙編器總是從前到後把彙編程式碼轉換成目標檔案,在這個過程中維護一個地址計數器,當處理到每個段的開頭時把地址計數器置成0,然後 每處理一條彙編指示或指令就把地址計數器增加相應的位元組數,在彙編程式中,可以取出當前地址計數器的值,是一個常量。      在_start中調了兩個系統呼叫,第一個是write系統呼叫,第二個是_exit系統呼叫。在調write系統呼叫時,eax暫存器儲存著write的系統呼叫號4,ebx,ecx,edx寄存 器分別儲存著write系統呼叫需要的三個引數。ebx儲存著檔案描述符,程序中每個開啟的檔案都有一個編號稱為檔案描述符,檔案描述符1表示標準輸出,對應於C標準 I/O庫的stdout,ecx儲存著輸出緩衝區的首地址,edx儲存著輸出的位元組數,write系統呼叫把從msg開始的len個位元組寫到標準輸出。      C程式碼中的write函式是系統呼叫的包裝函式,其內部實現就是把傳進來的三個引數分別賦給ebx,ecx,edx暫存器,然後執行movl $4,$eax和int $0x80兩條指令。這 個函式不可能完全用C程式碼來寫,因為任何C程式碼都不會編譯生成int指令,所以這個函式有可能是完全用於彙編寫的,也可能是用C內聯彙編寫的,甚至可能是一個巨集定 義(省了引數入棧出棧的步驟)。_exit函式也是如此。你可以通過man _exit查詢。 1.2 C標準I/O庫函式與Unbuffered I/O函式
     C標準I/O庫函式是如何用系統呼叫實現的。 fopen(3)      呼叫open(2)開啟指定的檔案,返回一個檔案描述符,(就是一個int型別的編號),分配一個FILE結構體,其中包含該檔案的描述符、I/O緩衝區和當前讀寫位置等信 息,返回這個FILE結構體的地址。 fgetc(3)      通過傳入的FILE*引數找到檔案的描述符,I/O緩衝區和當前讀寫位置,判斷是否從I/O緩衝區中讀取下一個字元,如果能讀到就直接返回該字元,否則呼叫read(2), 把檔案描述符穿進去,讓核心讀取檔案的資料到I/O緩衝區,然後返回下一個字元。注意,對於C標準I/O庫來說開啟的檔案由FILE*指標標識,而對於核心來說,開啟的 檔案由檔案描述標識,檔案描述符從open系統呼叫獲得,開啟的檔案由write、close系統呼叫時都需要穿檔案描述符。 fputc(3)      判斷該檔案的I/O快取區是否有空間在存放一個字元,如果有空間則直接儲存在I/O緩衝區中並返回,如果I/O緩衝區已滿就呼叫write(2),讓核心把I/O緩衝區的內容 寫回檔案。 fclose(3)      如果I/O緩衝區中還有資料沒有寫回檔案,就呼叫write(2)寫回檔案,然後呼叫close(2)關閉檔案,釋放FILE結構體和I/O緩衝區。      關閉檔案是為了將buffer中的內容寫回到(磁碟) open、read、write、close等系統函式稱為無緩衝I/O(Unbuffered I/O)函式,因為它們位於C標準庫的I/O緩衝區的底層。使用者程式在讀寫檔案時既可以呼叫C標準I/O庫 函式,也可以呼叫底層的Unbuffered I/O函式,那麼用哪一組函式好呢?      用Unbuffered I/O函式每次讀寫都要進核心,調一個系統呼叫比調一個使用者空間的函式要慢得多,所以在使用者空間開闢I/O緩衝區還是必要的,用C標準I/O庫函式就比 較方便,省去了自己管理I/O緩衝區的麻煩。      用C標準I/O庫函式要時刻注意I/O緩衝區和實際檔案有可能不一致,在必要時需要呼叫fflush(3)。      我們知道UNIX的傳統是Everything is a File,I/O函式不僅僅用於讀寫常規檔案,也用於讀寫裝置檔案,比如終端或網路裝置。在讀寫裝置時通常是不希望有緩衝區, 例如向代表網路裝置的檔案寫資料就是希望資料通過網路裝置傳送出去,而不是希望只寫到緩衝區裡就算完事了,當網路裝置收到資料時應用程式也希望第一時間被通 知到,所以網路程式設計通常直接呼叫Unbuffered I/O函式。 Unbuffered I/O介面是UNIX標準的一部分,所有UNIX系統的核心都要提供這組服務,但不是C標準庫的一部分,也就是說,在支援C語言的非UNIX作業系統上,標準I/O庫 的底層可以是另外一組函式(例如Win32 API的ReadFile、WriteFile)。      關於UNIX標準      現在改說說檔案描述符了。每個程序在核心中都有一個task_struct結構體來維護程序相關的資訊,在Linux核心中稱為程序描述符(Process Descriptor),而在作業系統 理論中稱為程序控制塊(PCB,Process Control Block)。tast_struct中包含該程序當前開啟的所有檔案的資訊,稱為檔案描述符表,在核心中files_struct結構體表示,其 中的表項稱為檔案描述符(File Descriptor),每個表項都包含一個指向已開啟的指標。      在使用者程式中檔案描述符指的是檔案描述符表的索引(即0、1、2、3這些數字),用int型變數來儲存。當呼叫open開啟一個現有檔案或建立一個新檔案時,核心分 配一個新的檔案描述符並返回給程序,當讀寫該檔案時,檔案描述符被作為引數傳遞給read或write。以前我們用C標準I/O函式,呼叫fopen開啟檔案,返回一個FILE * 指標,當讀寫該檔案時就傳遞這個FILE *指標。而fopen、fputc、fgetc的底層實現就要呼叫open、read、write。可見FILE結構體中必然包含檔案描述符,此外還包含快取 區的相關資訊,但FILE指標是不透明,我們不必關心這些資訊在FILE結構體中如何儲存和表示。      執行程式時會開啟三個檔案:標準輸入、標準輸出和標準錯誤輸出。在C標準庫中分別用FILE *指標stdin、stdout和stderr表示。這三個檔案的描述符分別是0,1,2,保 存在FILE結構體中。標頭檔案unistd.h中有如下的巨集定義來表示這三個檔案描述符: #define STDIN_FILENO 0 #define STDOUT_FILENO 1 #define STDERR_FILENO 2 1.3 open函式
open函式可以開啟或建立一個檔案。 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); pathname引數是要開啟或建立的檔名,和fopen一樣,pathname既可以是相對路徑也可以是絕對路徑,如果只有檔名沒有路徑就表示相對於當前工作目錄。flags 引數有一系列常數值可供選擇,可以同時選擇多個常數用按位或運算子連線起來,所以這些常數的巨集定義都是以O_開頭,表示or。 必選項:一下三個常數中必須指定一個,且僅允許指定一個。      O_RDONLY     只讀開啟      O_WRONLY     只寫開啟      O_RDWR     可讀可寫開啟 其它可選項介紹:      O_APPEND表示追加。如果檔案已有內容,這次開啟檔案所寫的資料附加到檔案的末尾而不是覆蓋原來的內容。      O_CREAT若此檔案不存在則建立它。使用此選項時需要提供第三個引數mode,表示該檔案的訪問許可權。      O_EXCL如果同時指定了O_CREAT,而檔案已經存在,則出錯。      O_TRUNC如果此檔案存在,而且為只寫或可讀可寫成功開啟,則將長度截斷(Truncate)為0.      O_NONBLOCK對於裝置檔案,以O_NONBLOCK方式開啟可以做非阻塞I/O(Nonblock I/O)。 1、以寫的方式fopen一個檔案時,如果檔案不存在會自動建立,以寫方式open一個檔案時,必須明確指定O_CREAT才會建立檔案,否則檔案不存在就出錯返回。 2、以w或w+方式open一個檔案時,如果檔案已存在就截斷,以寫方式open一個檔案時,必須宣告指定O_TRUNC才會截斷檔案,否則直接在原來的資料上改寫。 第三個引數mode是檔案的rwx許可權,可以用八進位制數表示,比如0644表示-rw-r--r--,也可以用S_IRUSR、S_IWUSR等巨集定義按位或起來表示詳見open(2)。要注意的是 檔案的許可權是由open的mode引數和當前程序的umask掩碼共同決定的。比如在Shell下設定umask掩碼為022,然後執行程式a.out open函式呼叫成功則返回值為檔案描述符,呼叫失敗則返回-1,同時設定error未相應的錯誤碼。由open返回的檔案描述符0、1、2,因此呼叫open開啟檔案就會返回描 述符3,再呼叫open就會返回4。可以利用這一點在標準輸入、標準輸出上開啟一個新的檔案,實現重定向功能。例如,首先關閉標準輸出(檔案描述符1),然後開啟 另一個檔案,那麼該檔案一定會被分配檔案描述符1。後面要講的dup2函式提供了一種更好的辦法在指定的檔案描述符上開啟檔案。 1.4 close函式 #include <unistd.h> int close(fd); 引數fd是要關閉的檔案描述符。close函式成功返回0,失敗返回-1。 需要說明的是,當一個程序終止時,即使不呼叫close,它開啟的所有檔案也被核心自動關閉。但是對於一個長年累月執行的程式(比如網路伺服器),開啟的檔案描 述符一定要記得關閉,否則隨著開啟的檔案越來越多,會佔用大量檔案描述符和系統資源。 1.5 read/write read函式從開啟的裝置或檔案中讀取資料。 #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); 引數count是請求讀取的位元組數,讀上來的資料存在緩衝區buf中,同時檔案中的讀寫位置向後移,返回值是讀到的位元組數,若讀操作之前已到達檔案末尾,則讀操作返 回0,若出錯則返回-1。返回值型別是ssize_t,表示有符號的size_t,這樣既可以返回正的位元組數、0(到達檔案末尾)也可以返回負值-1(出錯)。read函式返回時, 返回值決定了buf中的有效位元組數。有些情況將使實際讀到的位元組數(也就是返回值)小於count,例如:      讀常規檔案時,在讀到count位元組之前已到達檔案末尾。例如,距離檔案末尾還有30個位元組而請求讀100個位元組,則read返回30,下次read將返回0.      從終端裝置讀,通常以行為單位,讀到換行符就返回了。      從網路裝置讀,根據不同的傳輸層協議和核心快取機制,返回值可能小於請求的位元組數,後面socket程式設計部分詳細介紹。 write函式向開啟的裝置或檔案中寫入資料。 #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); 返回值是寫入的位元組數,若出錯則返回-1。寫常規檔案時,write的返回值通常等於請求寫的位元組數count,向裝置或網路寫入則不一定。 對常規檔案是不會阻塞的,不管讀多少位元組,read一定會有限的時間內返回。從終端和網路上沒有發來資料包,read一個socket就會阻塞,會阻塞多長時間也是不確定 的,如果一直沒有資料到達就一直阻塞在那裡。寫常規檔案也是不會阻塞的,而向裝置或網路寫則不一定。 阻塞讀終端例項 #include <unistd.h> #include <stdlib.h> #include <stdio.h> int main(void) {     char buf[10];     int n;     n = read(STDIN_FILENO, buf, 10);     if(n < 0)     {         perror("read STDIN_FILENO");         exit(1);     }     write(STDOUT_FILENO, buf, n);     printf("\n");     return 0; } 執行介面如下:
while(1) {      非阻塞read(裝置1)      if(裝置1有資料到達)           處理資料;      if(裝置2有資料到達)           處理資料;      ...... } 如果read(裝置1)是阻塞的,那麼只要裝置1沒有資料到達就會一直阻塞在裝置1的read呼叫上,即使裝置2有資料到達也不能處理,使用非阻塞I/O就可以避免裝置2 得不到及時處理。 非阻塞I/O有一個缺點,如果所有裝置都一直沒有資料到達,呼叫者需要反覆查詢做無用功,如果阻塞在哪裡,作業系統可以排程別的程序執行,就不會做無用功了。 在使用非阻塞I/O時,通常不會在一個while迴圈中一直不停地查詢(這個稱為Tight Loop),而是每延遲等待一會就來查詢一下,以免做太多無用功,在延遲等待的時 候可以排程其它程序執行。 while(1) {      非阻塞read(裝置1)      if(裝置1有資料到達)           處理資料;      if(裝置2有資料到達)           處理資料;      ......      sleep(n); } 這樣做的問題是,裝置1有資料到達時可能不能及時處理,最長需要延遲n秒才能處理,而是反覆查詢還是做無用功。以後要學習的select(2)函式可以阻塞地同時監視多 個裝置,還可以設定裝置等待的超時時間,從而圓滿地解決了這個問題。 一下是一個非阻塞I/O的例子。目前我們學過得可能引起阻塞的裝置只有終端,所以我們用終端來做這個實驗。程式開始執行時在0、1、2檔案描述符上自動開啟的檔案 就是終端,但是沒有O_NONBLOCK標誌。我們可以重新開啟一遍裝置檔案/dec/tty(表示當前終端),在開啟時指定O_NONBLOCK標誌。 非阻塞讀終端例項 #include <stdio.h> #include <unistd.h> #include <errno.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #define MSG_TRY "try again\n" int main(void) {     int buf[10];     int n, fd;     fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);     if(fd < 0)     {         perror("open /dex/tty error!\n");         exit(1);     } tryagain:     n = read(fd, buf, 5);     if(n < 0)     {         /*相等時表示非阻塞成功直接跳出*/         if(errno != EAGAIN)         {             perror("read /dev/tty error!\n");             exit(1);         }         sleep(1);         write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));         goto tryagain;     }     write(STDOUT_FILENO, buf, n);     close(fd);     return 0; } 一下是用非阻塞I/O實現等待超時的例子。既保證了超時退出的邏輯又保證了有資料到達時處理延遲較小。 #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <stdlib.h> #define MSG_TRY "try again\n" #define MSG_TIMEOUT "timeout\n" int main(void) {     char buf[10];     int fd, n;     int i = 0;     fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);     if(fd < 0)     {         perror("open /dev/tty error!\n");         exit(1);     }     for(i = 0; i < 5; i++)     {         n = read(fd, buf, 10);         if(n >= 0)         {             break;         }         if(errno != EAGAIN)         {             perror("read /dev/tty error!\n");             exit(1);         }         sleep(1);         write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));     }     if(n == 5)     {         write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));     }     else     {         write(STDOUT_FILENO, buf, n);     }     close(fd);     return 0; } 1.6 lseek函式 每個開啟的檔案都記錄當前讀寫位置,開啟檔案時讀寫位置是0,表示檔案開頭,通常寫多少位元組就會將讀寫位置往後移多少個位元組。但是有一個例外,如果以 O_APPEND方式開啟,每次寫操作都會在檔案末尾追加資料,然後將讀寫位置移動到新的檔案末尾。lseek和標準I/O庫的fseek函式類似,可以移動當前讀寫位置(或 者叫偏移量)。 #include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); 引數offset和whence的含義和fseek函式完全相同。只不過第一個引數換成了檔案描述符。和fseek一樣,偏移量允許超過檔案末尾,這種情況下對該檔案的下一次寫 操作將延長檔案,中間空洞的部分讀出都是0. 若lseek成功執行,則返回新的偏移量,因此可用以下方法確定一個開啟檔案的當前偏移量: off_t currpos; currpos  =  lseek(fd,  0,  SEEK_CUR); 這種方法也可用來確定檔案或裝置是否可以設定偏移量,常規檔案都可以設定偏移量,而裝置一般是不可以設定偏移量。如果裝置不支援lseek,則lseek返回-1,並 將errno設定為ESPIPE。注意fseek和lseek在返回值上有細微的差別,fseek成功時返回0失敗時返回-1,要返回當前偏移量需要呼叫ftell,而lseek成功時返回當前偏移量 失敗時返回-1. 1.7 fcntl 先前我們以read終端裝置為例介紹非阻塞I/O,為什麼我們不直接對STDIN_FILENO做非阻塞read,而要重新open一遍/dev/tty呢?因為STDIN_FILENO在程式啟動時已 經被自動打開了,而我們需要在呼叫open時指定O_NONBLOCK標誌。這裡介紹另一種辦法,可以用fcntl函式改變一個已開啟的檔案的屬性,可以重新設定讀、寫、追 加、非阻塞等標誌(這些標誌稱為File Status Flag),而不必重新open檔案。 #include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd, struct flock *lock); 這個函式和open一樣,也是用可變引數實現的,可變引數的型別和個數取決於前面的cmd引數。下面的例子使用F_GETFL和F_SETFL這兩種fcntl命令改變STDIN_FILENO 的屬性。加上O_NONBLOCK選項。實現非阻塞讀終端同樣的功能。 #include <unistd.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> #include <errno.h> #define MSG_TRY "try again!\n" int main(void) {     char buf[10];     int n;     int flags;     flags = fcntl(STDIN_FILENO, F_GETFL);     flags |= O_NONBLOCK;     if(fcntl(STDIN_FILENO, F_SETFL, flags)  == -1)     {         perror("fcntl error!\n");         exit(1);     } tryagain:     n = read(STDIN_FILENO, buf, 10);     if(n < 0)     {         /*非阻塞失敗*/         if(errno != EAGAIN)         {             perror("read error!\n");             exit(1);         }         sleep(1);         write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));         goto tryagain;     }     write(STDOUT_FILENO, buf, n);     return 0; } 一下程式通過命令列的第一個引數指定一個檔案描述符,同時利用Shell的重定向功能在該描述符上開啟檔案,然後用fcntl的F_GETFL命令取出File Status Flag並列印。 #include <sys/types.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) {     int val;     if(argc != 2)     {         fputs("usage: a.out <descriptor#>\n", stderr);         exit(1);     }     if((val = fcntl(atoi(argv[1]), F_GETFL)) < 0)     {         printf("fcntl error for fd %d\n", atoi(argv[1]));         exit(1);     }     switch(val & O_ACCMODE)     {         case O_RDONLY:             printf("read only");             break;         case O_WRONLY:             printf("write only");             break;         case O_RDWR:             printf("read write");             break;         default:             fputs("invalid access mode!", stderr);             exit(1);     }     if(val & O_APPEND)         printf(", append\n");     if(val & O_NONBLOCK)         printf(", nonblocking\n");     return 0; } 1.8 iotcl iotcl用於向裝置發控制和配置命令,有些命令也需要讀寫一些資料,但這些資料是不能用read/write讀寫的,稱為Out-of-band資料。也就是說,read/write讀寫的資料是 in-band資料,是I/O操作的主體,而ioctl命令傳送的是控制資訊,其中的資料是輔助的資料。例如,在串列埠上收發資料通過read/write操作,而串列埠的波特率、校驗位、 停止位通過iotcl設定,A/D轉換的結果通過read讀取,而A/D轉換的精度和工作頻率通過iotcl設定。 #include <sys/ioctl.h> int ioctl(int d, int request, ...); d是某個裝置的檔案描述符。request是ioctl的命令,可變引數取決於request,通常是一個指向變數或結構體的指標。若出錯則返回-1,若成功則返回其他值,返回值也是取決於request。 以下程式使用TIOCGWINSZ命令獲得終端裝置的視窗大小 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> int main(void) {     struct winsize size;     /*判斷終端是否開啟*/     if(isatty(STDOUT_FILENO) == 0)         exit(1);     if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0)     {         perror("iotcl TIOCGWINSZ error!\n");         exit(1);     }     printf("%d rows, %d columns\n", size.ws_row, size.ws_col);     return 0; } 1.9 mmap mmap可以把磁碟檔案的一部分直接對映到記憶體,這樣檔案中的位置直接就有對應的記憶體地址,對檔案的讀寫可以直接用指標來做而不需要read/write函式。 #include <sys/mman.h> void *mmap(void  *addr,  size_t  len,  int  port,  int  flag,  int  fileds,  off_t  off); int munmap(void  *addr,  size_t  len); 如果addr引數為NULL,核心會自己在程序地址空間中選擇合適的地址建立對映。如果addr不是NULL,則給核心一個提示,應該從什麼地址開始對映,核心會選擇addr之 上的某個合適的地址開始對映。建立對映後,真正的對映首地址通過返回值可以得到。len引數是需要對映,必須是頁大小的整數倍(在32位體系系統結構上通常是4k) filedes是代表該檔案的描述符。 port引數有四種取值。      PRO T_EXEC表示對映的這一段可執行,例如對映共享庫      PROT_READ表示對映的這一段可讀      PROT_WRITE表示對映的這一段可寫      PROT_NONE表示對映的這一段不可訪問 flag引數有很多種取值,這裡只講兩種,其它取值可檢視mmap(2)      MAP_SHARED多個執行緒對同一個檔案的對映是共享的,一個程序對對映的記憶體做了修改,另一個程序也會看到這種變化。      MAP_PRIVATE多個程序對同一個檔案的對映不是共享的,一個程序對對映的記憶體做了修改,另一個程序並不會看到這種變化,也不會真的寫到檔案中。 如果mmap成功則返回對映首地址,如果出錯則返回常數MAP_FAILED。當程序終止時,該程序的對映記憶體會自動解除,也可以呼叫munmap解除對映。munmap成功返 回0,出錯返回-1。 #include <stdlib.h> #include <sys/mman.h> #include <fcntl.h> int main(void) {     int *p;     int fd = open("hello", O_RDWR);     if(fd < 0)     {         perror("open hello error!\n");         exit(1);     }     p = mmap(NULL, 6, PROT_WRITE, MAP_SHARED, fd, 0);     if(p == MAP_FAILED)     {         perror("mmap error!\n");         exit(1);     }     close(fd);     p[0] = 0x30313233;     munmap(p, 6);     return 0; }