国外很多牛人出了很多牛书,他们既是科研界的先驱,也是教育界的领军人物,他们的书值得我们反复看反复研究,《Unix环境高级编程》就是其中之一,很久之前就看过一部分,还写了一篇如何运行该书配套程序的博客,最近一段时间在Linux下开发很多,未来很长一段时间可能会继续使用ARM+Linux进行开发。这篇博客时对前一个博客的补充,不过这本书简直就是Linux编程的词典,内容很多,这里重点介绍第一章的内容。
0.Unix和Linux是啥
如果要写Unix和Linux历史,那足以写一本《三国演义》一样厚的传记。不过天下大事,分久必合合久必分,Unix和Linux也是一样,我们就不再赘述其历史,感兴趣可以自己查。操作系统的作用,我认为就两个,一个是封装硬件,一个是向上层提供各种服务。之前使用过ARM M3的板子,可以直接用裸机程序开发,但是如果要实现一些高级的功能就必须使用回调等比较复杂的方法,而移植了操作系统,很多工作就变得简单。当然,系统也只能移植只有几个文件的FreeRTOS,或者UCosII这种非常小的操作系统,但是个人感觉只要有了操作系统的支持,上层的开发就容易好多。不过难点是如果想在一个低端的设备上移植操作系统,也不是一件容易的工作。
对于高级设备,操作系统也是一种软件,能够控制计算机的硬件资源,并且为程序提供运行环境。对于Linux这种大型的软件系统,其本身又是分层次的。内核相对较小是系统的核心,提供最基础的系统引导和最基本的硬件驱动加载,稍微高级一点的功能和系统库都由系统调用层来完成。系统调用是承上启下的作用,即扩展了内核的功能,又为应用开发提供了强大的支持。大部分操作系统的差异就体现在系统调用层的差异上。例如windows系统和Linux系统的类库就完全不一样(当然了,都是C/C++代码)。
Linux还有一种特殊的结构shell,它是一种特殊的应用程序,它可以作为用户程序的接口,甚至可以在用户程序里直接调用shell命令。其实shell就是一个解析命令的工具,类似于早期的DOS命令。windows其实就是将DOS系统的命令换成了大量的图形化界面,而Linux系统由于其应用环境的特殊性,各类命令仍然具有强大的活力。
1.文件和目录
#include <apue.h>
#include <dirent.h>
#include<my_err.h>
int
main(int argc, char *argv[])
{
DIR *dp;
struct dirent *dirp;
if (argc != 2)
err_quit("usage: ls directory_name");
if ((dp = opendir(argv[1])) == NULL)
err_sys("can't open %s", argv[1]);
while ((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);
closedir(dp);
exit(0);
}
然后 gcc 1.1.c生成a.out.
如果想看详细的解释,就请仔细阅读书第5页的内容。
2.输入和输出
#include <apue.h>
#include<unistd.h>
#include<my_err.h>
#define BUFFSIZE 4096
int
main()
{
int n;
char buf[BUFFSIZE];
while((n=read(STDIN_FILENO,buf,BUFFSIZE))>0)
if(write(STDOUT_FILENO,buf,n)!=n)
err_sys("write error");
if(n<0)
err_sys("read error");
exit(0);
}
这里比较难理解的是STDIN_FILENO和STDOUT_FILENO,字面意思是标准输入和输出的文件编号。程序运行后能接受用户的输入并直接显示出来。例如
#include <apue.h>
#include<unistd.h>
#include<my_err.h>
int
main()
{
int c;
while((c=getc(stdin))!=EOF)
if(putc(c,stdout)==EOF)
err_sys("output error");
if(ferror(stdin))
err_sys("input error");
exit(0);
}
1.3程序和进程
#include <apue.h>
int
main(void)
{
printf("hello world from process ID %d\n",getpid());
exit(0);
}
编译后输入./a.out就可以看到下面的输出:
#include <apue.h>
#include<sys/wait.h>
#include"my_err.h"
int
main(void)
{
char buf[MAXLINE];
pid_t pid;
int status;
printf("%%");
while(fgets(buf,MAXLINE,stdin)!=NULL)
{
if(buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1]=0;
if((pid=fork())<0){
err_sys("fork error");
}else if(pid==0){
execlp(buf,buf,(char*)0);
err_ret("could not execure %s",buf);
}
if((pid=waitpid(pid,&status,0))<0)
err_sys("waitpid error");
printf("%%");
}
exit(0);
}
编译后执行./a.out,可以输出某些命令的执行结果,例如ls、date
现在来介绍几个重点函数。首先是fork(不是fuck),其功能是创建一个新进程。事实上,很多进程的结构、功能等都是类似的,没有必要每次完全建造新的,新的进程可以复制某个进程来完成。被调用的进程和新进程是父子关系(我感觉母女关系更好,O(∩_∩)O~,有点龌龊啊)。fork在创建新进程时,向父进程返回新子进程的ID号。
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
在本书的后面会专门研究这些函数。这些函数的作用是利用一个新的进程镜像(process image)来替代当前的进程镜像。主要说明一下execlp函数,它的第一个参数file用于制定可执行的文件名,第二个参数arg以及后面的可变参都是传递给该可执行文件的参数,由于是可变参数,所以最后一个参数必须以(char*)NULL结尾。该函数如果成功执行,由于是替代当前的进程,所以函数调用语句之后的代码就不会再被执行了。这里需要注意的是传递的参数,一般情况下,execlp函数的前两个参数一样,都是可执行文件名。我们考虑带参数的main函数的执行情况,argv[0]指代的实际上是可执行文件名,其后的argv[1]~~argv[n]才是真正传递给它的参数。所以为了保持一致性,由于arg参数以及其后的可变参才是传递给执行程序的参数,所以,arg参数最好传递可执行文件的名字,就是是和file参数保持一致。
子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这样可以保证所有的进程都在系统的控制之下,这一功能就是waitpid实现的,其参数指定要等待的进程的。waitpid函数返回子进程的终止状态,如果需要可以通过该值判断子进程的终止时间。