應用執行個體的編寫實際上已經不屬於Linux作業系統移植的範疇,但是為了保證本系列文章的完整性,這裡提供一系列針對嵌入式Linux開發應用程式的執行個體。
編寫Linux應用程式要用到如下工具:
(1)編譯器:GCC
GCC是Linux平台下最重要的開發工具,它是GNU的C和C++編譯器,其基本用法為:gcc [options] [filenames]。
我們應該使用arm-linux-gcc。
(2)調試器:GDB
gdb是一個用來調試C和C++程式的強力調試器,我們能通過它進行一系列調試工作,包括設定斷點、觀查變數、單步等。
我們應該使用arm-linux-gdb。
(3)Make
GNU Make的主要工作是讀進一個文字檔,稱為makefile。這個檔案記錄了哪些檔案由哪些檔案產生,用什麼命令來產生。Make依靠此 makefile中的資訊檢查磁碟上的檔案,如果目的檔案的建立或修改時間比它的一個依靠檔案舊的話,make就執行相應的命令,以便更新目的檔案。
Makefile中的編譯規則要相應地使用arm-linux-版本。
(4)代碼編輯
可以使用傳統的vi編輯器,但最好採用emacs軟體,它具備文法高亮、版本控制等附帶功能。
在宿主機上用上述工具完成應用程式的開發後,可以通過如下途徑將程式下載到目標板上運行:
(1)通過串口通訊協定rz將程式下載到目標板的檔案系統中(感謝Linux提供了rz這樣的一個命令);
(2)通過ftp通訊協定從宿主機上的ftp目錄裡將程式下載到目標板的檔案系統中;
(3)將程式拷入隨身碟,在目標機上mount 隨身碟,運行隨身碟中的程式;
(4)如果目標機Linux使用NFS檔案系統,則可以直接將程式拷入到宿主機相應的目錄內,在目標機Linux中可以直接使用。
1. 檔案編程
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);
讀寫
在檔案開啟以後,我們才可對檔案進行讀寫了,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);
下面我們來編寫一個應用程式,在目前的目錄下建立使用者可讀寫檔案"example.txt",在其中寫入"Hello World",關閉檔案,再次開啟它,讀取其中的內容並輸出在螢幕上:
#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);
}
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函數則返回零,這就是一個函數返回兩次的本質。
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標誌用於設定建立子進程時的相關選項。
來看下面的例子:
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指定的子進程退出。
Linux的處理序間通訊(IPC,InterProcess Communication)通訊方法有管道、訊息佇列、共用記憶體、訊號量、套介面等。通訊端通訊並不為Linux所專有,在所有提供了TCP/IP協議棧的作業系統中幾乎都提供了socket,而所有這樣作業系統,對通訊端的編程方法幾乎是完全一樣的。管道分為有名管道和無名管道,無名管道只能用於親屬進程之間的通訊,而有名管道則可用於無親屬關係的進程之間;訊息佇列用於運行於同一台機器上的處理序間通訊,與管道相似;共用記憶體通常由一個進程建立,其餘進程對這塊記憶體區進行讀寫;訊號量是一個計數器,它用來記錄對某個資源(如共用記憶體)的存取狀況。
下面是一個使用訊號量的例子,該程式建立一個特定的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);
}
3. 線程式控制制/通訊編程
Linux本身只有進程的概念,而其所謂的"線程"本質上在核心裡仍然是進程。大家知道,進程是資源分派的單位,同一進程中的多個線程共用該進程的資源(如作為共用記憶體的全域變數)。Linux中所謂的"線程"只是在被建立的時候"複製"(clone)了父進程的資源,因此,clone出來的進程表現為"線程"。Linux中最流行的線程機製為 LinuxThreads,它實現了一種Posix 1003.1c "pthread"標準介面。
線程之間的通訊涉及同步和互斥,互斥體的用法為:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); //按預設的屬性初始化互斥體變數mutex
pthread_mutex_lock(&mutex); // 給互斥體變數加鎖
… //臨界資源
phtread_mutex_unlock(&mutex); // 給互斥體變數解鎖
同步就是線程等待某個事件的發生。只有當等待的事件發生線程才繼續執行,否則線程掛起並放棄處理器。當多個線程協作時,相互作用的任務必須在一定的條件下同步。Linux下的C語言編程有多種線程同步機制,最典型的是條件變數(condition variable)。而在標頭檔semaphore.h 中定義的訊號量則完成了互斥體和條件變數的封裝,按照多線程程式設計中存取控制機制,控制對資源的同步訪問,提供者設計人員更方便的調用介面。下面的生產者/消費者問題說明了Linux線程的控制和通訊:
#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 16
struct prodcons
{
int buffer[BUFFER_SIZE];
pthread_mutex_t 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;
}
4.小結
本章主要給出了Linux平台下檔案、進程式控制制與通訊、線程式控制制與通訊的編程執行個體。至此,一個完整的,涉及硬體原理、Bootloader、作業系統及檔案系統移植、驅動程式開發及應用程式編寫的嵌入式Linux系列講解就全部結束了。