所有操作系统都为运行在它之上的程序提供各种服务,典型的服务包括:执行新程序、打开文件、读写文件、分配存储空间、提供时间等。
UNIX体系结构
严格来说,操作系统是一种软件,它控制计算机硬件资源,提供程序运行环境。这种软件有个专业术语名称:内核。因为它小且位于计算机体系的核心。如下图所示:
内核的接口为系统调用,系统调用包裹在内核的外围,隔离开内核以保护内核。同时,系统调用作为和内核沟通的中间桥梁。
-
公用函数库通常指的是C/C++的标准库,例如libc、glibc、libstdc++、libc++等标准库,不同的标准库是由于不同的系统平台或编译器厂商造成的差异,但追根究底都是使用了底层系统内核的API接口系统调用。这些公用函数库极大的方便了上层开发者的使用,也是整个计算机软件体系(无论任何编程语言)的基础。
-
shell也是一个软件,该软件负责人机交互,用户和shell进行“对话”,然后shell理解用户的意图来使计算机按用户的想法工作。
-
用户程序也可以直接使用系统调用来请求服务。
对于打印hello world于终端中,对于上面三种请求服务的方式,有:
printf("hello world");
echo 'hello world'
- 直接使用
write()
登录
用户名和shell由配置文件/etc/passwd决定,密码由配置文件/etc/shadow决定。
口令文件中的登录项由 :
分隔的7个字段组成:
- 登录名
- 加密口令
- 用户ID(数字)
- 组ID(数字)
- 注释字段
- 起始目录
- shell程序
例如,我电脑Mac OSX中就有:
root:*:0:0:System Administrator:/var/root:/bin/sh
文件系统
UNIX文件系统是目录和文件的一种层次结构。所有东西的起点称为根目录,即“/”。
在文件系统中,目录本质上也是一个文件,其内容是目录项的记录。每个目录项都是一个文件名,还包含一些文件属性的说明信息,比如权限、大小、时间等。
创建新目录时文件系统会自动创建两个文件夹:.(点)和 ..(点点), .(点)表示当前目录,根目录“/”下的.(点)和 ..(点点)是同一个路径,都是“/”。
由斜线“/”开头的路径都是绝对路径,反之则是相对路径。
ls(1)
ls(1)指的是第一部分的ls项目,UNIX命令通常都会有一个说明手册,手册中对命令有详细说明,但随着命令功能和说明的增加,说明手册页数越来越多,之后便对命令进行了分门别类,通常是1-8总共8中分类,具体是:
1、Standard commands (标准命令)
2、System calls (系统调用)
3、Library functions (库函数)
4、Special devices (设备说明)
5、File formats (文件格式)
6、Games and toys (游戏和娱乐)
7、Miscellaneous (杂项)
8、Administrative Commands (管理员命令)
工作目录:每个进程都有一个工作目录,一般称为当前工作目录,进程可以用chdir来更改其工作目录。
起始目录:登录时,shell程序的工作目录设置为home目录,该目录通常从/etc/passwd配置文件中获得。
输入和输出
在UNIX系统中输入和输出是经过抽象的,所有的输入和输出底层系统实现都是通过文件抽象来完成的。
文件描述符:文字描述符是一个小的非负整数,内核用以标识一个特定进程正在存访的文件。当内核打 开一个现存文件或创建一个新文件时,它就返回一个文件描述符。当读、写文件时,就可使 用它。
每个程序都有默认打开的三个文件描述符:
- 标准输入 - fd为0
- 标准输出 - fd为1
- 标准出错 - fd为2
不带缓存的I/O:系统调用open
、read
、write
、lseek
、close
,它们都使用文件描述符来操作文件,并且不带缓冲。
标准I/O:公用函数库提供的封装过的接口来间接调用系统调用,公用函数库提供的接口是带缓冲的,且无需考虑对缓冲区大小的选择。例如使用printf
输出。
程序和进程
程序是静态的进程,而进程是运行着的程序。程序本质上是一个存在硬盘上的可执行文件。程序被加载到内存中之后就开始执行,此时程序变成一个动态刻画抽象的进程。
进程ID:每一个进程都有一个标识符,称为进程ID,其是一个非负数,且在当前时刻是唯一的。
进程控制:有3个可以用于控制进程的系统调用:fork、exec和waitpid。其中exec是一系列函数的统称。
线程和线程ID:
一个进程内的所有线程共享当前进程的所有内存空间、文件描述符号、栈以及进程相关的属性。由于所有进程共享进程的内存空间,因此在访问共享数据时需要采取同步措施以避免数据的不一致。
同进程类似,线程也有一个ID唯一标识每一个线程,但线程的ID只在进程内部有效,进程外部则无意义。
出错处理
当 UNIX 系统函数出错时,通常会返回一个负值,同时整型变量 errno
通常被设置为具有特定信息的值。
- 文件
<errno.h>
定义了errno
以及赋予它的各种常量,这些常量以E
字符开头 - 在多线程环境中,每个线程都有属于自己的局部
errno
,以避免一个线程干扰另一个线程。
对于 errno
的使用要注意两条规则:
- 如果没有出错,则
errno
的值不会被清除.因此只有在函数的返回值指明出错了时,检查errno
才有意义 - 任何函数都不会将
errno
的值清零,且在<errno.h>
中定义的所有常量都不为0
strerror/perror
函数:用于处理错误信息
#include<string.h>
char *strerror(int errnum);
#include<stdio.h>
void perror(const char*msg);
- strerror: 将
errnum
(通常就是errno
值)映射为一个出错消息字符串,并且返回此字符串的指针- 参数: 一个整数(通常是
errno
的值) - 返回: 出错消息字符串的指针
- 参数: 一个整数(通常是
- perror: 基于
errno
的当前值,在标准错误上产生一条出错消息,然后返回。这条出错消息首先是msg
指向的字符串,后面是冒号,后面是一个空格,后面是对应于errno
值的出错消息,最后是一个换行符。- 参数:附加的出错消息
- 返回:无返回。但是向标准错误上输出一条出错消息。
用户标识
用户ID:用户标识也是通过ID来进行区分的,该ID称为用户ID,它是一个数字。当一个用户创建时,会在/etc/passwd文件中生成唯一的用户ID,用户不能更改这个ID,除非是root用户才允许修改。ID号码为0的用户是root用户。
组ID:用户除了用ID来进行区分,也用组来进行划分管理,相应的,其也有组ID,也是一个数字。用户的组用户ID不唯一,一个用户可以拥有多个组ID,这表明该用户加入了多个小组。组的目的是为了让多个用户共享一个资源。组相关的配置文件是/etc/group。
可以通过 getuid( )
和 getgid( )
函数来获得相应的用户ID和组ID。
信号
信号是UNIX系统用于发送通知的一种机制,例如,若某一进程执行除法操作,其除数为0,则将名为SIGFPE的信号发送给该进程。进程收到信号通知后,有3种应对处理方法:
-
忽略信号。收到之后什么也不做,当做未发生一样。
-
按系统默认方式处理。对于除0,系统默认方式是终止该进程。
-
提供一个处理函数。在收到信号之后,用提供的函数来进行处理。
使用Kill -9 杀死进程
ctrl + c 中断进程
ctrl + / 退出进程
时间值
UNIX 系统使用两种时间:
- 日历时间:自 UTC 1970年1月1日 00:00:00 以来经历过的秒数累计值。用
time_t
数据类型来保存这种时间值。 - 进程时间:也称作CPU时间,用于度量进程使用的CPU资源。进程时间以时间滴答来计算,用
clock_t
数据类型保存这种时间值。
当度量一个进程的执行时间时,UNIX系统为一个进程维护了3个进程时间值:
- 时钟时间: 又称作墙上时钟时间,是进程运行的时间总量,其值与系统中同时运行的进程数有关
- 用户CPU时间:执行用户指令所用的时间量
- 系统CPU时间:该进程执行内核程序所经历的时间。如进程执行一个
read
系统调用,则内核执行该系统调用的时间计入系统 CPU 时间
用户CPU时间和系统CPU时间之和称作 CPU 时间
系统调用和库函数
所有的UNIX系统都提供多种服务的入口点,由此程序可以向内核请求服务。各种UNIX都提供了良好定义、数量有限、直接进入内核的入口点。这些入口点被称为系统调用。
系统调用接口在man手册的第二部分中说明,是使用C语言定义的。
公用函数库接口在man手册的第三部分中说明,也是使用C语言定义的。它们不一定是内核的入口点,部分会间接使用一个或多个内核系统调用,而有些则完全不使用。
从实现角度看,系统调用和公用函数库有着本质区别,一个是伴随内核而产生的,是不可替换的。另一个是编译器厂商根据语言标准而实现的,可以更新和替换。但从用户角度看,它们没有太大区别,显著的区别是公用函数库更好用,功能更加强。