宋宝华谈 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 系列讲解就全部结束了。
原文链接:http://dev.yesky.com/131/2531131.shtml。