在学习Gatech CS6200课程时,课程项目多处使用了POSIX,例如Project 3 Part 使用POSIX api实现IPC。故打算了解一下POSIX起源和发展。
POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX )
本篇博客内容来自参考的文章,不过在文章基础上进行了排班的简化,让思路更加清晰
起源
1974年,贝尔实验室正式对外发布Unix。因为涉及到反垄断等各种原因,加上早期的Unix不够完善,于是贝尔实验室以慷慨的条件向学校提供源代码,所以Unix在大专院校里获得了很多支持并得以持续发展。
于是出现了好些独立开发的与Unix基本兼容但又不完全兼容的OS,通称Unix-like OS。
包括:
- 美国加州大学伯克利分校的Unix4.xBSD(Berkeley Software Distribution)。
- 贝尔实验室发布的自己的版本,称为System V Unix。
- 其他厂商的版本,比如Sun Microsystems的Solaris系统,则是从这些原始的BSD和System V版本中衍生而来。
20世纪80年代中期,Unix厂商试图通过加入新的、往往不兼容的特性来使它们的程序与众不同。
局面非常混乱,麻烦也就随之而来了。
为了提高兼容性和应用程序的可移植性,阻止这种趋势, IEEE(电气和电子工程师协会)开始努力标准化Unix的开发,后来由 Richard Stallman命名为“Posix”。
这套标准涵盖了很多方面,比如Unix系统调用的C语言接口、shell程序和工具、线程及网络编程。
- 所以说这个POSIX实际上就是定义了调用操作系统的C语言的接口
- shell程序就是我们使用的终端,POSIX定义每个命令对应什么系统指令呢之类的。如下图
Unix和Linux之后都遵循这个规范
有了这个规范,你就可以调用通用的API了,Linux提供的POSIX系统调用在Unix上也能执行,因此学习Linux的底层接口最好就是理解POSIX标准。
库函数API和系统调用的区别
上面的system call和 function call分别就是系统调用和调用库函数API;
glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。这些基本函数都是被标准化了的,而且这些函数通常都是用汇编直接实现的。
glibc 为程序员提供丰富的 API(Application Programming Interface),这些API都是遵循POSIX标准的,API的函数名,返回值,参数类型等都必须按照POSIX标准来定义。
POSIX兼容也就指定这些接口函数兼容,但是并不管API具体如何实现。
如上图所示:
- (1) 库函数是语言或应用程序的一部分,而系统调用是内核提供给应用程序的接口,属于系统的一部分
- (2) 库函数在用户地址空间执行,系统调用是在内核地址空间执行,库函数运行时间属于用户时间,系统调用属于系统时间,库函数开销较小,系统调用开销较大
- (3) 系统调用依赖于平台,库函数并不依赖
系统调用是为了方便使用操作系统的接口,而库函数则是为了人们编程的方便。
库函数调用与系统无关,不同的系统,调用库函数,库函数会调用不同的底层函数实现,因此可移植性好。
可移植性
那么目标代码和启动代码是怎么生成的呢? 答案是编译器。
编程语言编写的程序首先要被编译器编译成目标代码(0、1代码),然后在目标代码的前面插入启动代码,最终生成了一个完整的程序。
要注意的是,程序中为访问特定设备(如显示器)或者操作系统(如windows xp 的API)的特殊功能而专门编写的部分通常是不能移植的。
综上所述,一个编程语言的可移植性取决于
- 不同平台编译器的数量
- 对特殊硬件或操作系统的依赖性
移植是基于操作系统的。但是这个时候,我们需要注意一点:基于各种操作系统平台不同,应用程序在二级制级别是不能直接移植的。
我们只能在代码层去思考可移植问题,在API层面上由于各个操作系统的命名规范、系统调用等自身原因,在API层面上实现可移植也是不大可能的。
在各个平台下,我们默认C标准库中的函数都是一样的,这样基本可以实现可移植。但是对于C库本身而言,在各种操作系统平台下其内部实现是完全不同的,也就是说C库封装了操作系统API在其内部的实现细节。
因此,C语言提供了我们在代码级的可移植性,即这种可移植是通过C语言这个中间层来完成的。
例如在我们的代码中下功夫。以下代码可以帮助我们实现各平台之间的可移植:
#ifdef _WINDOWS_
CreateThread(); //windows下线程的创建
#else
Pthread_create(); //Linux下线程的创建
#endif
对于头文件,也使用同样的预编译宏来实现。如:
#ifndef _WINDOWS_
#include <windows.h>
#else
#include <thread.h>
#endif
这样就可以实现代码的可移植了。在编译的时候只要通过#define就可以选择在那个平台下完成程序的编译。
综上所述,我们都是将C,C++等各种语言当作中间层,以实现其一定程度上的可移植。如今,语言的跨平台的程序都是以这样的方式实现的。但是在不同的平台下,仍需要重新编译。
系统开销
使用系统调用会影响系统的性能,在执行调用时的从用户态切换到内核态,再返回用户态会有系统开销。
为了减少开销,因此需要减少系统调用的次数,并且让每次系统调用尽可能的完成多的任务。(系统提供标准函数库可以减少)
硬件也会限制对底层系统调用一次所能写的数据块的大小。
为了给设备和文件提供更高层的接口,Linux系统提供了一系列的标准函数库。
使用标准库函数,可以高效的写任意长度的数据块,库函数在数据满足数据块长度要求时安排执行底层系统调用。
一般地,操作系统为了考虑实现的难度和管理的方便,它只提供一少部分的系统调用,这些系统调用一般都是由C和汇编混合编写实现的,其接口用C来定义,而具体的实现则是汇编,这样的好处就是执行效率高,而且,极大的方便了上层调用。
随着系统提供的这些库函数把系统调用进行封装或者组合,可以实现更多的功能,这样的库函数能够实现一些对内核来说比较复杂的操作。
比如,read()函数根据参数,直接就能读文件,而背后隐藏的比如文件在硬盘的哪个磁道,哪个扇区,加载到内存的哪个位置等等这些操作,程序员是不必关心的,这些操作里面自然也包含了系统调用。POSIX就是定义了这些标准函数库。
而对于第三方的库,它其实和系统库一样,只是它直接利用系统调用的可能性要小一些,而是利用系统提供的API接口来实现功能(API的接口是开放的)。
这段话比较复杂,大概就是操作系统也有自己通过C和汇编实现一些标准的函数库,这个库里的函数封装了比较复杂的系统调用,这样每次调用它系统调用也可以尽可能完成多的任务,而且上层调用不用再处理复杂的系统调用,所以操作系统只提供一小部分直接的系统调用,更多的可能是这种标准函数库的函数。
例子
如下图是Linux系统调用的大概流程。
当应用程序调用printf()函数时,printf函数会调用C库中的printf,继而调用C库中的write,C库最后调用内核的write()。
而另一些则不会使用系统调用,比如strlen, strcat, memcpy等。
printf函数执行过程中,程序运行状态切换如下:
用户态–>系统调用–>内核态–>返回用户态
printf函数、glibc库和系统调用在系统中关系图如下:
实例代码如下:
1 #include <stdio.h>
2
3
4 int main(int argc, char **argv)
5 {
6 printf(yikoulinux);
7 return 0;
8 }
编译执行
root@ubuntu:/home/peng/test# gcc 123.c -o run root@ubuntu:/home/peng/test# strace ./run
root@ubuntu:/home/peng/test# gcc 123.c -o run
root@ubuntu:/home/peng/test# strace ./run
如执行结果可知: 我们的程序虽然只有一个printf函数,但是在执行过程中,我们前后调用了execve、access、open、fstat、mmap、brk、write等系统调用。 其中write系统调用会把字符串:yikoulinux通过设备文件1,发送到驱动,该设备节点对应终端stdout。
【注意】运行程序前加上strace,可以追踪到函数库调用过程