第一章:UNIX基础知识
一、unix体系结构 |
严格来说,操作系统可定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核。因为它小且位于计算机体系的核心。
下图显示了unix系统的体系结构:
上图阴影部分为系统调用,所有的系统调用都会从用户空间中汇聚到 0x80中断点,同时保存具体的系统调用号。
c语言中的open()、read()等函数都是通过系统调用触发中断,进而调用驱动函数完成读写操作。
二、文件和目录 |
unix文件系统是一直树形层次结构,所有文件的起点是一个为根的目录,它是“/”。在unix系统中本着是一切皆文件的思想,比如在命令行中执行如下命令,就会显示/etc目录的下的文件和目录。
$ vi /etc
创建新目录时会自动创建两个文件夹:.(点)和..(点点)。.(点)表示当前目录,..(点点)表示父目录。有一个特殊的情况,那就是根目录“/”下的.(点)和..(点点)是同一个路径,都是“/”。
由斜线“/”开头的路径都是绝对路径,反之则是相对路径。
下面是一个用c实现的ls命令代码:
1 #include <dirent.h> 2 #include <stdio.h> 3 4 int main(int argc, char const *argv[]) 5 { 6 if (argc != 2) 7 { 8 printf("usage: %s <directory>\n", argv[0]); 9 return -1; 10 } 11 12 dir *dp; 13 struct dirent *dirp; 14 15 if (!(dp = opendir(argv[1]))) 16 { 17 printf("can't open %s\n", argv[0]); 18 return -2; 19 } 20 21 while ((dirp = readdir(dp))) 22 { 23 printf("%s\n", dirp->d_name); 24 } 25 26 return 0; 27 }
我们可以通过结果看到/etc目录下有.(点)和..(点点)两个目录。(仅展示部分结果)
$ ./a.out /etc
.
..
bluetooth
dbus-1
rsyslog.conf
三、输入和输出 |
在unix系统中输入和输出是经过抽象的,所有的输入和输出都是通过文件来完成的。当我们读写时,是在对文件进行读写,而实际上该文件可能管理映射到硬件(如led、按键等),也可能是一个socket套接字。
文件的抽象是通过文件描述符实现的,打开一个文件得到一个文件描述符,它通常是非负数的,我们使用read()、write()读写时,都是对文件描述符进行操作。
标准输入、标准输出和标准错误也是三个文件描述符,且一般情况下,它们被shell默认打开并默认被系统映射到硬件设备。我们可以使用“<”和“>”来重定向这三个文件描述符默认打开的设备。比如执行之前的命令:
$ ./a.out > /dev/null
此时我们会发现命令行中没有输出,因为此命令将输出结果重定位到/dev/null空设备中。
四、程序和进程 |
程序是静态的进程,而进程是运行着的程序。程序本质上是一个存在硬盘上的可执行文件。程序被加载到内存中之后就开始执行,此时程序变成一个动态的进程。每一个进程都有一个标识符,称为进程id,其是一个非负数,且在当前时刻是唯一的。
有3个可以用于控制进程的系统调用:fork、exec和waitpid。其中exec是一系列函数的统称。
每一个进程都是一个独立的个体。一个进程可以拥有多个线程。
通常,一个进程只有一个主线程,也就是main函数的线程。当我们需要同时处理多个任务时(比如我们一边听歌、一边走路),就需要使用多线程。一个进程内的所有线程共享当前进程的内存空间、文件描述符、栈和进程相关的属性。由于所有进程共享进程的内存空间,因此在访问共享数据时需要采取同步措施以避免数据的不一致。
同进程类似,线程也有一个id唯一标识每一个进程,但线程的id只在进程内部有效,进程外部则无意义。
五、出错处理 |
当unix系统调用函数出错时,通常会返回一个负值。一般我们需要对出错进行处理。
系统调用函数通常会将错误返回值赋给errno,errno变量看起来像是一个int类型的变量,但实际上并不是。
早期的时候,它被简单的用int类型变量实现。但随着多线程出现之后,一个进程的errno变量是被多个线程共享的,当某一个线程因为出错而改变了errno变量之后,其他线程无法根据errno来判断自己当前的状态,造成了混淆,因此现在它通常被实现为一个函数调用。
extern int *__errno_location(void); #define errno (*__errno_location())
c标准定义了两个函数,可用于打印出错信息。
#include <string.h> char* strerror(int errnum); #include <stdio.h> void perror(const char *msg);
六、信号 |
信号(signal)用于通知进程发生了某种状况(比如执行除数为0的除法操作),则系统会发送一条通知至该进程,进程收到信号通知后,有3种应对处理方法:
- 忽略信号。不进行处理。
- 按系统默认方式处理。
- 提供一个函数。此方式在收到信号之后,用我们提供的函数进行处理。
举个例子,假设现在有一个程序,它有三种方式来处理用户通过键盘ctrl+c(对应信号的sigint)发出的中断信号。
对于忽略信号,程序会忽略ctrl+c,导致按ctrl+c时没有任何反应。
对于按系统默认方式处理,ctrl+c在系统中默认是终止程序,则程序会被终止。
对于提供一个函数,程序使用我们提供的函数进行处理,我们可以在函数中执行printf()等操作。
七、时间值 |
unix中有两种表示时间的方式。
一种是指日历时间(比如现在是几点几分),该值是从1970年1月1日以来经过了多少秒的形式。
另一种是指进程时间,用来表示进程执行的时间。
我们可以用time命令来得知一个程序执行所花费的时间:
$ time ./a.out > /dev/null
八、系统调用和库函数 |
所有的操作系统都提供多种服务的入口点,由此程序可以向内核请求服务。各种版本的unix实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用。
系统调用接口在man手册的第二部分中说明,是使用c语言定义的。比如:
$ man 2 read
公用函数库接口在man手册的第三部分中说明,也是使用c语言定义的。它们不一定是内核的入口点,部分会间接使用一个或多个内核系统调用,而有些则完全不使用。
从实现角度看,系统调用和公用函数库有着本质区别,系统调用是伴随内核而产生的,在用户空间是不可替换的。公用函数库是编译器厂商根据语言标准而实现的,可以更新和替换。
从用户角度看,它们没有太大区别。
上一篇: 进程池与线程池
下一篇: 西红柿汤的做法大全介绍,有哪些营养价值?