UNIX system
UNIX是一类操作系统,所有的操作系统都为它们运行的程序提供服务。典型的服务包括:执行新程序,读写文件,分配存储区域,获得当前时间等等。
UNIX体系结构
严格意义上来说,可以把操作系统定义为一种系统软件,它控制计算机硬件资源,提供程序运行环境,这种软件称为内核。从广义上来说,操作系统包含了内核和其它一些软件,这些软件使得计算机能够发挥作用,其他软件包括应用程序,shell和公共函数库等。比如,Linux是GNU操作系统的内核,我们有时候也把GNU/Linux操作系统称为Linux,所以Linux可以表示两层含义,一种是内核,一种是操作系统。
内核的接口称为系统调用(system call),公用函数库在系统调用接口之上,应用程序可以使用公用函数库,也可以使用系统调用。shell是一个特殊的应用程序,它为运行其他应用程序提供了一个接口。
系统调用提供了访问内核的接口,公用函数库构建在系统调用上,应用程序可以构建在公用函数库上,也可以构建在shell和系统调用之上,shell建立在系统调用之上。
登录(Login)
用户名(username),用户组(group)和密码(passwd)
/etc/group和/etc/passwd文件分别给出了user的group和passwd相关信息,这些文件的具体格式可以使用man 5 passwd
和man 5 group
查看。
shell
shell是一个命令行解释器,它读取用户输入,然后执行命令。shell的用户输入通常来自终端,这种是交互式shell;有时shell的输入也可以来自文件,这种文件叫做shell脚本。
常见系统的shell有:
- Bourne shell,/bin/sh,是Steve Bourne在贝尔实验室开发的shell,几乎所有的UNIX系统都支持这种shell,
- Bourne-again shell, /bin/bash,GNU shell,所有的Linux系统都提供这种shell,它的设计遵循了POSIX标准,同时保留了和Bourne shell的兼容性。更多关于/bin/bash的内容可以参考linux /bin/bash。
- C shell,/bin/csh,Bill Joy在博客里开发的shell,所有BSD版本都提供这种shell
- Korn shell,/bin/ksh,贝尔实验室的David Korn开发的,在大多数UNIX系统上运行,与Bourne shell向上兼容。
- TENEX C shell,/bin/tcsh,是C shell的加强版本,它从TENEX系统借鉴而来,常用来替换C shell。
不同的Linux版本使用不同的默认shell,一些Linux默认使用Bourne-again shell,另外一些使用BSD对Bourne shell的替代品dash(Debian Almquist shell)。
FreeBSD的默认用户shell衍生于Debian Almquist shell。
Mac OS X的默认shell是Bourne-again shell。
Solaris继承了BSD和System V,它提供了所有的shell。
文件和目录(File and dir)
文件系统
UNIX文件系统是文件和目录的一种树状结构,所有目录和文件的起点是根目录(root dir),用"/"表示。
文件名
目录中的各个名字称为文件名,只有"/“和空格不能出现在文件名中。”/“用来分隔构成路径名的各文件名,而空格用来终止一个路径名。
创建新的目录时会自动创建”.“目录指向当前目录和”…“指向父目录,在根目录中,”.“和”…"都指向根目录。
目录名
由斜线分隔的一个或者多个文件名组成的序列构成路径名,以斜线开头的路径名称为绝对路径名,否则是相对路径名。"/"是一个特殊的绝对路径名,它不包含文件名。
工作目录
相对路径名都是从工作目录开始解释的。在shell中可以通过cd更改工作目录,进程可以通过chdir更改工作目录。
起始目录(用户主目录)
用户登录后,工作目录默认设置为用户主目录,可以从/etc/passwd文件查看。
输入和输出(Input and Output)
文件描述符
文件描述符是一个小的非负整数,内核用它来标示一个进程正在访问的文件。当内核打开或者创建一个文件时,都会返回一个文件描述符。在读写文件时,可以使用这个文件描述符。
标准输入,标准输出和标准错误
在shell中运行一个新程序时,所有的shell都会为这个程序打开三个文件描述符,标准输入,标准输出和标准错误。如果没有指定的话,它们都链接向终端。可以通过重定向将它们链接到文件。
不带缓冲区的I/O
函数open
,read
,read
,lseek
和close
提供了不带缓冲的I/O。这些函数都使用文件描述符。
头文件<unistd.h>
包含了两个常量STDIN_FILENO
和STDOUT_FILENO
,分别指向标准输入和标准输出的文件描述符。它们是POSIX标准的一部分,在POSXI标准中分别是0和1,但是为了可读性,一般不用它们的具体值。
标准I/O
标准I/O函数为那些不带缓冲的I/O函数提供了一个带缓冲的借口。使用标准I/O不用担心如何选取最佳的缓冲区大小,还简化了对输入行的处理。
程序和进程(Process)
程序
程序是一个存储在磁盘上的某个目录中的可执行文件。内核使用exec
函数将程序读入内存,并执行该程序。
进程和进程ID(PID)
程序的执行实例被称为进程(process)。每一个进程都有一个唯一的数字标示符,叫做进程ID(process ID, PID),PID是一个非负整数。可以使用getpid
函数获得PID,getpid
返回一个pid_t
类型,我们不知道它的大小,但是标准保证它能存放在一个长整形中,所以可以把它强制转换成它可能会用到的最大数据类型即长整形,提高程序的可移植性。
进程控制
有三个用于进程控制的主要函数,他们分别是fork
,exec
和waitpid
。
fork
创建一个新进程,新进程是调用进程的一个副本,称调用进程为父进程,新创建的进程为子进程。fork
对父进程返回子进程的进程ID(一个非负整数),对子进程则返回0。execlp
函数要求参数以null结束而不是换行符结束。- 子进程中调用
execlp
执行从标准输入读入的命令,用新的程序文件代替子进程原先执行的程序文件。fork
和exec
两者组合就是某些操作系统所称的spawn一个新进程。在UNIX系统中,这两部分分成了两个独立的函数。 - 子进程调用
execlp
执行新的程序文件,父进程希望等待子进程终止,这是通过waitpid
实现的,waitpid
接收要等待进程的ID,以及接收一个整形的地址,用来存放子进程的终止状态。出错返回-1,否则返回state改变的子进程的PID,或者返回0(子进程的状态没有改变)。
线程(thread)和线程I/O
- 通常来说一个进程只有一个控制线程,即某一时刻执行的一组机器指令。对于某些问题,如果有多个控制线程分别多用于它的不同部分,可以使得问题变得更加容易。也可以充分利用多处理器系统的并行能力。
- 一个进程内的所有线程共享同一地址空间,文件描述符,栈以及和进程相关的属性。因为线程能够访问同一个存储区域,所以线程在访问共享数据时需要采取同步措施避免不一致性。
- 线程也有ID,但是线程ID只在它所属的进程内起作用。一个进程中的线程ID在另一个进程中没有意义。当一个进程中对某个特定线程进程处理时,可以使用线程ID引用它。
出错处理
当UNIX系统函数出错时,通常会返回一个负值,整形变量errno通常被设置为具有特定信息的值。比如对于open
函数,errno大约有15种不同的值(文件不存在,权限问题),成功执行返回一个非负文件描述符,出错则返回-1。另外有一些函数对于出错使用约定值而不是返回值。比如返回对象指针的函数,出错时会返回一个NULL指针。
头文件<errno.h>
定义了errno以及可以赋值给它的各种常量。这些常量都以字符E开头。可以使用man 2 intro
列出这些常量,在linux中使用man 3 errno
列出这些常量。
**关于errno的两条规则:
- 如果没有出错,errno的值不会被清除,所以,只有在函数的返回值指明出错的时候,errno的值才是有意义的,否则不要使用它;
- 任何函数都不会将errno的值设置为0,而且
<errno.h>
头文件中的任何常量都不等于0。**
出错恢复
可以将<errno.h>
中定义的各种出错分为两类:致命性的和非致命性的。
对于致命性的错误,无法恢复,可以输出错误信息帮助用户处理。
对于非致命的错误,可以妥善的进行处理。大多数非致命性出错都是暂时的,比如因为资源短缺。常见的与资源相关的非致命性出错包含:EAGAIN
,ENFILE
,ENOBUFS
,ENOLCK
,ENOSPC
,EWOULDBLOCK
,有时候ENOMEM
也是非致命性出错。当EBUSY
指明共享资源正在被使用时,也可以将它作为非致命性出错。当EINTR
中断一个慢速调用时,也可将它作为非致命性出错处理。对于资源相关的非致命性出错,最典型的恢复操作是延迟一段时间重试。
用户标识
用户ID(UID)
用于标识不同的用户。可以在口令文件/etc/passwd中找到用户的UID。root用户的UID是0。
组ID(GID)
用于标识不同的group。口令文件/etc/passwd中也包含了用户的GID。组文件将组名映射为数值的GID。
为什么使用数值的UID和GID,对于磁盘上的每个文件,都应该存储该文件所有者的GID和PID,存储这两个值只需要4个字节(假设每个都用双字节的整形存放,早期的UNIX系统使用16位整数表示UID和GID,而现在的UNIX系统使用32位整数表示UID和GID。
)。如果使用ASCII user name和group name,需要更多的磁盘空间。在进行权限检验时,整形也比字符串更快。
但是对于用户来说,使用ASCII name比较方便,所以在/etc/passwd和/etc/group中存放了UID和user name以及GID和group name的映射。
附属组ID
每个登录名除了/etc/passwd指定的一个group外,还可以属于其他的group,大多数系统至少支持16个附属组,通过查找/etc/group中有该登录名的前16个项作为它的附属组ID。
信号(signal)
Signal用于通知进程发生了某种特殊情况。比如进程在执行除法操作的时候,发生了除0操作,内核会将SIGFPE signal发送给进程。对于每一个signal,进程有以下三种方式进行处理:
- 忽略某个signal。不推荐这种处理方式,如果发生了除0操作,就会出错。
- 按系统默认方式处理。比如除0操作,系统默认是终止进程。
- 提供一个函数,某个signal发生时,调用该函数,这称为捕捉signal。
很多情况都会产生signal,通过键盘有两种产生signal的方法:
- 中断键(interrupt key),通常是ctrl+C或者Delete键和退出建(quit key),通常是Ctrl+\ 键,这两个signal用于中断当前运行的进程。
- 调用kill函数,向进程发送一个signal,注意我们必须是root用户或者这个进程的所有者。
捕获signal示例
在bash中运行程序时,使用中断键,相应的程序会终止。为什么会发生这种结果,对于中断信号(SIGINT)的系统默认动作是终止进程,因为进程没有告诉系统内核该怎么处理,所以系统按照默认方式终止该进程。
为了捕获该信号,程序需要调用signal函数,其中指定了产生SIGINT信号时要调用的函数的名字。
时间(time)
UNIX系统使用过两种不同的时间值。
- 日历时间。协调世界时(Coordinated Universal Time, UTC),即从1970年1月1日00:00:00这个时间开始经过的累计秒数。
系统基本数据类型time_t用于保存这种时间值。 - 进程时间。也称为CPU时间,用来度量进程使用的CPU资源。进程时间用始终滴答计算,每秒钟曾经取过50,60或者100个时钟滴答。
系统基本数据类型clock_t保存这种时间值。
当度量一个进程的执行时间时,UNIX系统为一个进程维护了三个进程时间值:
- 时钟时间,进程运行的时间总量,它的值和系统中同时运行的进程数有关。APUE中用到的时钟时间指的都是系统中没有其他活动时进行度量的。其实就是这个进程从开始到结束总共花了多长时间,包括阻塞,等待和运行的时间。
- 用户CPU时间,执行用户指令所用的时间。
- 系统CPU时间,为该进程执行内核程序所经历的时间。
用户CPU时间和系统CPU时间被称为CPU时间,可以使用time命令获得一个进程的时钟时间,用户CPU时间和系统CPU时间。比如:1
2~$:time ls
~$:time man ls
系统调用(system call)和库函数(library function)
系统调用
所有的操作系统都提供多种服务的入口点,通过这些入口点向内核请求服务,这些入口点被称为系统调用(system call)。系统调用处于kernel mode,一些任务只能在kernel mode运行。比如和硬件的交互,系统调用使得用户mode的进程可以通过系统调用进入kernel mode,从而实现和硬件的交互。。
系统调用接口可以在man的第二部分查看,它是用C语言定义的,与具体系统如何调用一个系统调用的实现技术无关。这些和早期的操作系统按照传统方式用机器的汇编语言定义内核入口点。
UNIX使用的方法是为每个系统调用在标准C库中设置一个同名函数。用户进程使用标准C调用相应的函数,这些函数又根据系统调用调用相应的内核服务。
库函数
库函数可以在man手册的第三部分查看,第三部分定义了程序员可以使用的通用库函数。库函数可以调用系统调用,也可以不调用系统调用,比如read
函数会调用系统调用,而atoi
等并不使用任何系统调用。
系统调用和库函数之间的关系
- 从实现角度来看,系统调用和库函数有着根本的区别,系统调用处于内核mode,库函数属于用户mode。
- 从用户应用角度考虑,可以把系统调用看做C函数,使用系统调用还是库函数不重要,它们都是为应用程序提供服务的。
- C函数只是系统调用和库函数的一种实现,系统调用和库函数都可以以其他方式实现。
- 系统调用通常只是提供一种最小接口,而库函数实现更复杂的功能。
- 库函数可以被替换,但是系统调用通常是不能替换的。
- 库函数可以调用系统调用,也可以不调用系统调用。
- 应用程序既可以调用库函数也可以调用系统调用。
- 进程控制系统调用(fork, exec和wait)等通常由应用程序直接调用。为了简化一些常见情况,UNIX也提供了一些库函数,如system和popen。
- 库函数链接到用户程序,在user space执行,而syste call没有链接到用户程序,在kernel space执行
- 库函数的执行时间被计算为user level time,而system call的执行事件算作system time的一部分。
- 库函数可以简单的进行debug,而系统调用不能debug,因为它们被kernel执行。
对于第4条,可以考虑以下例子:
sbrk(2)是分配存储空间的UNIX系统调用,它按照指定字节数增加或者减少进程地址空间。如何管理进程的地址空间由进程决定。
malloc(3)是公用函数库中的一个存储分配空间函数,它负责进行进程的存储地址管理。
我们可以自己实现一个malloc,但是它很有可能还要使用sbrk(2)。内核中的系统调用ssbrk是系统层面的空间分配,而库函数malloc是在用户层面进行操作。
参考文献
1.《APUE》第三版
2.https://www.thegeekstuff.com/2012/07/system-calls-library-functions/
3.https://stackoverflow.com/questions/29816791/what-is-the-difference-between-system-call-and-library-call