ARM的嵌入式Linux移植體驗之應用實例

應用實例的編寫實際上已經不屬於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)將程序拷入U盤,在目標機上mount U盤,運行U盤中的程序;

(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系列講解就全部結束了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章