欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

第一章:UNIX基础知识

程序员文章站 2022-06-01 17:22:44
严格来说,操作系统可定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核。因为它小且位于计算机体系的核心。 下图显示了UNIX系统的体系结构: 上图阴影部分为系统调用,所有的系统调用都会从用户空间中汇聚到 0x80中断点,同时保存具体的系统调用号。 C语言中的open( ......

 

一、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种应对处理方法:

  1. 忽略信号。不进行处理。
  2. 按系统默认方式处理。
  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语言定义的。它们不一定是内核的入口点,部分会间接使用一个或多个内核系统调用,而有些则完全不使用。

从实现角度看,系统调用和公用函数库有着本质区别,系统调用是伴随内核而产生的,在用户空间是不可替换的公用函数库是编译器厂商根据语言标准而实现的,可以更新和替换

从用户角度看,它们没有太大区别。