前语:本人是半路出家做程序员,实际上应付平时工作中的业务还可以,但是基础知识实在薄弱,当然也跟中国计算机教育有关系,平时跟同事聊天,实际上就算是科班出身,对于计算机本身的了解也知之甚少,因此在毕业两周年到来的时候,给自己确定了以后的技术学习方向,同时也决定了将基础部分补充起来,特来CSDN开这个专题,去学习程序员圣经般著作—《UNIX高级环境编程》,这本书也是被某个同事经常提起,书本比较厚,但是不能操之过急,但是也不要指望一次性就能学完学透,没事回来看看,总有收获,自勉之。
一、UNIX体系结构
操作系统是为了在它们之上运行的程序提供服务的,从严格意思上来说,可以将操作系统看成是个软件,它负责来控制计算机的硬件资源,提供程序运行环境,从这个角度来说,其实还是比较容易理解操作系统的概念,比如说你要读取一个文件,必然是需要通过读取磁盘这种硬件资源才能完成,虽然我们平时写代码可能只需要执行下open或者read等函数就可以完成文件读取,但是真实情况不会那么简单,简单理解可以认为当我们执行了前述函数,实际上是向操作系统发起了一个任务,当任务完成后任务返回结果了,这个说法可能不够严谨,但是比较容易理解。这时候内核这个概念就出来了,而内核与外界程序的中介就是系统调用,在我们学着写C语言代码的时候,会有许多封装好的库函数,这些公用函数库主要就是构建在系统调用至上的,应用程序则既可以调用公用函数库也可以直接调用系统调用,在unix系统中,有一个特殊的应用程序叫着shell程序,这个程序是用户与系统的接口,通过shell我们可以执行其他的应用程序,这几个部分组成的体系结构图如下:
二、文件
这章节之前书中还提到了登录与shell,这两部分我在这里就不讲了,主要原因是用户这块影响最大的实际上是权限问题,后面书中其他章节会涉及到这块,讲的也比这里深入,所以暂且跳过。文件这部分对UNIX及其重要,或许许多人知道,unix的信条之一就是一切皆文件,也就是说无论是设备交互,文件读取,网络连接都可以看成或者抽象成一个文件对待,这块在以后的学习中通过了解API可以知道,太多的unix的东西接口都是很类似和相像的,原因也在这里。
这部分书中讲了几个概念,也就是概念,先了解了解就可以了,第一章的大部分后面会有专门的一个章节来专门讲解。UNIX的文件系统是一种层级结构,起点就是"/"这个根目录。在这里可能有个概念不是好理解,那就是UNIX文件系统是包含目录和文件的,但是在UNIX里面,目录实际上是一个包含目录项的文件,对应了上面一切皆文件的话了。逻辑上说,每个目录项都是一个结构体,里面包含了一个文件名,同时还包含了说明文件属性的信息。文件属性记载着这个文件各种属性的集合,这里面有一项是来指明文件类型的,说明这个文件是目录还是普通文件。
目录中的各个名字成为文件名,只有斜线和空字符不能出现在文件名中,这是因为斜线是用来分隔路径的,空字符用来终止一个路径名的,当创建一个新的目录时,会在该目录下创建两个文件名,分别是"."和"..",分别用来表示指向当前目录和父目录,当然在最高层次的根目录中,他们两都指向当前目录。而由斜线分隔的一个或者多个文件名构成的是路径名,以"/"开始的叫做绝对路径名,否则是相对路径名。比如说"/usr/local/bin",这里面usr,local,bin实际上都是文件名,虽然他们在实际系统里面都是文件夹(目录),但是在这个体系里面,就是文件名。而"/usr/local"连起来的就是路径名,而且是绝对路径,至于"local/bin"则是相对于/usr/的相对路径名。书中提供了一个ls命令简单版本的代码,用来去了解目录层级,大家可以看输出结果,我把书中原来的头文件给去掉了,这样不管谁想只是看看这段代码效果,不需要去把该书配的源码下载下来看,代码如下:
#include <stdlib.h> #include <stdio.h> #include <dirent.h> int main(int argc, char* argv[]) { DIR *dp; struct dirent *dirp; if (argc != 2) { printf("usage: ls directory_name"); return 0; } if ((dp = opendir(argv[1])) == NULL) { printf("can't open %s", argv[1]); return 0; } while ((dirp = readdir(dp)) != NULL) { printf("%s\n", dirp->d_name); } closedir(dp); exit(0); }
当我们登录系统的时候,实际上都会进入到自己登录用户的一个默认目录,这个叫起始目录,这部分在口令文件中可以看到,还有当我们运行程序的时候都会有一个工作目录,这也是为啥当我们把一个文件放到执行文件的路径下就可以读取的原因,当然我们也可以在程序中修改自己的工作目录,这个部分在后面介绍进程的时候会突出讲到。
三、输入输出
输入输出对计算机系统而言太重要了,也算是最直接最基础的功能,用途广泛,在UNIX系统尤其如此,无论是管道、终端,还是文件、socket,都离不开输入输出这个范畴。这部分也有几个比较重要的概念需要了解,这对于以后进一步深入学习非常重要,首先是文件描述符,这是内核用来标识一个特定进程正在访问的文件,注意,这里说的很清楚,就是文件描述符是跟进程相关的,不同进程操作同一个文件,文件描述符不一定一样,当内核打开一个已有或者创建新文件的时候,就会返回一个文件描述符,而后其他所有的操作,都是针对这个文件描述符来进行。
但是也有例外,对于所有程序而言,当运行该程序的时候,shell都会为这个程序打开3个文件描述符(0,1,2),分别是标准输入,标准输出以及标准错误,如果不做处理,这三个描述符都是链接到终端的,当然如果你愿意,完全可以通过"<"或者">"将这个三个重定向到某个文件,比如ls > 1.txt就会把ls的结果输出到1.txt文件中去。
讲完文件,再讲就是对文件的操作,实际上基本上都是读写操作,I/O操作分为不带缓冲的IO和带缓冲的IO,这两部分都会有专门的章节后面详细讲述,下面的代码只是让你大概了解下IO操作的过程:
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #define BUFFSIZE 4096 int main(void) { int n; char buf[BUFFSIZE]; while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) { if (write(STDOUT_FILENO, buf, n) != n) { printf("write error !\n"); } } if (n < 0) { printf("read error !\n"); } exit(0); }
四、程序与进程
这可能是我们最熟悉的部分了,也是我们最经常干的事情,写段代码,跑一下。写完的代码的就是程序,程序实际上就是一个可执行文件,存放在磁盘里面的某个地方,当我们想跑一下的时候,通过内核将该文件读取到内存,然后按照该文件中指令执行,执行的一个实例叫做进程,也就是说如果同时执行两个同样的程序,这时候是有两个进程的,每个进程都会有一个唯一的编码,叫进程ID,程序可以通过getpid函数获取自己当前的进程ID。刚才提到,内核是会把程序文件读入到内存,然后再执行,实际上这个过程就是exec函数的执行过程,exec是进程控制函数的一个,进程控制函数不多,主要就3个,分别是用来执行程序的exec函数,创建进程的fork函数以及waitpid函数,这部分后面也有几个章节来重点介绍。
#include <stdlib.h> #include <stdio.h> #include <unistd.h> int main(void) { printf("hello world from process ID %ld\n", (long) getpid()); exit(0); }
接下来要说下线程的事情,通常,一个进程只有一个控制线程,但是如果有时候能有多个控制线程分别干其他的事情,那么解决问题的能力就好多了,实际上,线程的概念并不是一开始就有的,所以线程模型的实现与进程模型在很多时候是非常相像的,这个后面章节介绍线程的时候也会提到,连控制线程的函数都与控制进程的函数相类似。一个进程内的所有线程共享同一个地址空间、文件描述符、栈以及进程相关的属性,这个就为了多个任务处理同一个事件提供了可能,同时也带来了一些实现上的困难,那就是多个线程在访问共享数据的同时如何保证数据的一致性,这也是令广大程序员头疼的锁的根源。
五、出错处理
调用过linux库函数的时候都比较熟悉,尤其是read等函数,如果出错的时候都会返回一个负值用来告知用户出错,同时整型变量errno会被设置为具有特定消息的值,通过该值不仅可以知道当前错误类型,还能获取到当前错误的描述,这块只需要注意一点,那就是多线程程序中errno的实现,这部分大家可以搜索相关技术帖了解下,即系统是如何保证同一个进程中当两个线程同时发生错误时保证都能输出对应的错误。
#include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> int main(int argc, char *argv[]) { fprintf(stderr, "EACCES: %s\n", strerror(EACCES)); errno = ENOENT; perror(argv[0]); exit(0); }
六、信号
信号是用于通知进程发生了某种情况。这部分请记住一点,信号实际上是系统唯一与进程发生联系的方法方式。这一条很重要,实际上在许多开源的软件上都应用到了这个技术,比如说如果一个服务程序的配置项修改了,如果非常优雅的更新呢,重启肯定不适合,定时获取,这时候就涉及间隔问题,或者如何快速生效,这时候信号就可以做到悄无声息了,所以但凡与进程交互的,都可以往信号上想想。
进程有三种信号处理方式:
1)忽略信号。
2)默认处理,比如说kill程序。
3)自定义信号捕捉函数,这也是前面提到与进程交互的方法的实现方式。这部分也会有一个章节来专门介绍。
整个文章实际上只是简单介绍了UNIX系统的一些基本概念,这些概念其实构成了系统对外的功能框架,后续可能还有网络通信等内容进一步展示,由于本人也是刚刚开始学习,许多知识理解还比较浅,所以介绍的不够明白和严谨,希望大家多多批评,共同进步!