Unix环境高级编程笔记

1 系统调用和库函数调用是有区别的,系统调用只提供最小的接口,而库函数则提供更为复杂的功能。例如获取当前日期,系统调用只会返回距离格林威治时间1970年1月1号经历了的秒数,而库函数怎会结合当地的时区计算出日期。


2 文件io的几个主要的接口分别是open\create\read\write\lseek\close(这几个接口属于不带缓冲的io,每次调用对应一次系统调用,陷入内核)。对内核而言,所有打开的文件都可以通过文件描述符来引用。当调用open\creat时都会返回一个非负数的文件描述符,这个文件描述符会作为read\write接口的参数。打开文件时可以指定读写模式,只读、只写或读写。

close用来关闭文件描述符,释放系统资源。进程退出时,也会隐式地调用这个接口。

每个打开的文件都会对应一个当前文件偏移量,读写都是从这个偏移位作为起点。lseek可以设定偏移位,不发生实际IO操作。当指定文件偏移位大于文件长度时,再调用write会产生文件空洞。文件空洞一般不会占用存储空间,无需分配磁盘块。

关于read的性能,内核会根据read的缓冲区大小,触发预读策略。一开始会随着缓冲区变大效率提升,最终达到一个峰值。如果缓冲区过大,有时候反而影响性能。

3 文件共享,先看打开文件的内核数据结构



 内核为每个进程维护一个打开文件描述符表,每行记录指向一个文件表。文件表维护了当前被打开文件状态的标志、文件当前偏移量,还包含了一个指向v节点的指针。v节点可以理解为linux中的inode,inode包含了文件的元数据信息,包括文件作者、数据块在磁盘所在位置的指针。inode在打开文件时就被加载到内存当中,可以快速的读取到。

 并发修改

扫描二维码关注公众号,回复: 1198301 查看本文章



 

当两个进程同时打开一个文件时,由于都指向同一个v节点,如果在同一个偏移位并发修改的话,那么很有可能会导致数据覆盖,比如都seek到同一个位置,然后调用write。

要避免这个问题可以使用通过打开文件时设定O_APPEND的方式,每次都在文件的尾部进行写操作,无需两步seek+write。

还有一种方式是采用pread/pwrite,他把seek+write合并成一个原子操作,他和两步调用还有一个区别,就是调用的时候无法中断定位和read操作,不更新文件指针。
 

4 dup操作用于复制文件描述符,会和原型指向同一个文件表项,共享状态。



 

5 一般来说进行IO操作时,内核都使用缓冲区,一般写操作都会先进入内核缓冲区。等缓冲区写满或者内核要用到缓冲区了才会把数据放到输出队列,到达队列头部才会进行实际的IO操作。这种方式叫做延迟写。

如果要保证写及时生效,那么必须用到sync\fsync\fdatasync。

sync会将缓冲区里面的数据排入写队列,不等实际的io操作完成就会直接返回。一般来说内核会有一个守护进程update,每隔30秒调用sync,定期清理缓冲区。

fsync适用于数据库的场景,只有当数据完全写入磁盘后才会返回。

fdatasync只会即时影响文件的数据写入。而fsync还会同时更新文件的属性。

fcntl可以修改已打开文件的性质,ioctl则提供更丰富的io操作。

6 linux的几种文件类型

  • 普通文件,可能是二进制也可能是文本,对内核来说无区别,对文件的解释由处理它的应用程序决定。
  • 目录文件,包含了其他文件的名字,还有指向其他文件的指针。对目录文件有读权限的可以看到目录下面的文件,但是只有内核才能直接写目录文件。
  • 块特殊文件,提供对设备(如磁盘)带缓冲的访问,每次以固定长度单位访问。
  • 字符特殊文件,提供不带缓冲的访问,访问长度可变。系统中所有的设备文件要么是块特殊文件,要么是字符特殊文件。
  • FIFO,命名管道,用于进程间通信。
  • 套接字,用于网络间进程通信。
  • 符号链接,这种文件指向另一个文件。

 7 每个打开的进程都会关联一组用户id,用于执行文件io时的权限检查。


而每个文件的访问都有权限控制,用9个位分别表示文件的owner、owner所在的group、其他other是否具有读写执行权限。

权限检查的规则有,要打开一个文件,必须对这个文件路径上所以目录都有执行权限。

8 文件系统的结构



 i节点维护一个引用计数,只有当引用计数为0时,文件才能被删除。

另外当关闭一个文件的时候,内核首先会检查是否有被进程打开,是否引用计数为0,如果都满足那么就会删除这个文件。


 9 标准io库封装了系统的io接口,比如说分配缓存区,这样用户就不必处理太多细节。

系统io接口是围绕文件描述符展开,而标准io则是基于流的调用方式(jdk里面的io接口也是直接和流打交道),流是更高层次的一个io抽象。

由于缓冲区大小会直接影响io的效率,标准io库帮助用户程序处理缓冲区分配的细节,通过缓冲区来减少实际io的次数,提高性能。不过他提供了很多种缓冲区模式,使用的时候需要特别注意。

  • 全缓冲,写操作填满缓冲区才会进行实际的io操作。或者用户自己调用flush进行清洗操作,刷盘。普通文件一般默认是全缓冲
  • 行缓冲,当遇到换行符时才进行实际的io操作,一般是面向终端的流会使用这种方式,比如标准输入。因为行缓冲毕竟缓冲区长度也是固定的,所以当缓冲区满的时候,即便还没遇到换行符也会进行实际io。
  • 不带缓冲,标准io不对字符进行缓冲存储,当调用fputs写n个字符时,相当于直接进行系统调用。一般标准出错文件使用不带缓冲,第一时间让用户看到错误信息。

使用标准io的时候,一定要了解他的缓冲区机制。

下图是性能对比,对98M的300万行的数据进行读取。第一行是直接使用系统调用,调整最优缓冲区大小的性能。系统cpu时间基本相同,因为对内核的读写请求数量基本一样。用户时间的不一样原因是不同接口的函数调用次数不一样,时钟时间是因为io等待时间的不同。

标准io库的性能不如直接系统调用的一个重要的原因是,进行的数据复制开销比较到,一次是内核和标准io的缓冲之间的复制,还有一次是标准io缓冲区和用户行缓冲区的复制。

尽管如此,应用一般不会因为使用了标准io库而影响性能,这毕竟不是瓶颈。



 10 口令文件维护了linux用户的信息,包括用户id,初始shell、加密口令等。为了提高安全性,加密口令一般会分开存储,放到阴影文件中。加密口令是通过单向算法加密的,无法从密文反推出明文。

用户登录linux、或者执行某些操作时都会使用到口令文件以及组文件,用于进行安全、权限判断。

11 内核执行c程序时,会调用exec函数,调用一个特殊启动例程,可执行文件以这个启动例程作为程序的起始地址。启动例程会从内核取得命令行参数和环境变量。

c程序的存储空间布局:

正文段,cpu执行的机器指令,正文段一般是只读的。

初始化数据段,包含程序里面明确赋予初始值的变量,比如int a=10.

非初始化数据段,包含那些只声明,未赋值的字段

栈,自动变量以及函数调用所需保存的信息都在这个段里。

堆,由程序动态分配的存储区域。(堆和栈的概念和java的类似)


12 内核用于控制进程有三个基本操作:创建进程、执行进程、终止进程。进程可以用进程id来唯一标识,进程结束后,进程id可以被后面新创建的进程复用。

进程调用fork创建子进程,子进程是父进程的一个副本,他可以获得父进程数据空间、堆、栈的副本,他们共享正文段、但是不共享存储空间。fork之后,父进程所有打开的文件描述符都被复制到子进程,父子进程每个打开的文件描述符共享一个文件表项。


尽管子进程会继承父进程的大部分属性,但是某些属性是不会继承的,比如说父进程的文件锁,父进程未处理的信号集。

fork也有失败的时候,比如系统的进程数量达到上限;实际用户的进程总数超过限制。

使用fork一般是以下两种场景:父进程需要一个复制自己的子进程,以执行不同的代码段,比如套接字服务,一个进程用于接收连接,一个进程用于处理连接数据;一个进程需要执行一个不同的程序,比如shell。


当一个进程正常结束或者异常终止,内核会向其父进程发送SIGCHLD信号;这种通知是一种异步的方式,父进程可以忽略,也可以使用信号处理程序来处理。父进程可以通过调用wait函数来获取已结束子进程的信息。当进程调用wait的时候,如果他所有的子进程都没有结束,那么wait会阻塞,如果有已结束的子进程,那么会返回这个子进程的状态信息,如果没有任何子进程,那么返回出错。

fork创建了新进程后,需要调用exec来执行新程序。执行exec之后进程id不会改变,它只是用一个新的程序来替换当前进程的正文段、数据、堆和栈段。

内核还提供了更换用户id的机制。一般来说,从安全来考虑,进程会按照最小特权的权限来设置用户id。有些时候进程要访问重要的文件,需要设置为超级用户以完成操作。操作完成后,为了保证安全性,又得把用户id修改为普通的用户角色。

大部分的unix系统还提供了进程会计的功能,当一个进程终止时,会把一些相关的信息记录下来,比如这个进程运行的用户时间、cpu时间、启动时间等,让我们有机会重新观察一个进程。

13 进程除了拥有进程id外,还会同时拥有进程组id。进程组是一个或者多个进程的组合,通常会与某个作业相关联。作业是指一组协作的进程完成用户的某些操作,这类操作一般会使用管道和重定向,例如cat  temp.txt| grep 'longji',这个作业会启动2个进程。

http://baike.baidu.com/view/573513.htm

14 信号是一种软件中断,提供了异步处理程序的一种重要的方法。

信号的产生有多种条件:

用户按住终端按键,引发某信号。比如ctrl+c引发中断信号。

硬件异常产生的信号,如除0,引用无效地址。这类信号由硬件检测得到,并发信号给正在执行异常的进程。

调用kill,可以发送信号给另一个进程或者进程组。kill用于关闭一个已经失控的进程。

当检测某些软件条件已经完成,也会为通知某进程而产生信号。比如sigurg(网络连接上传来带外数据),sigpipe(在一个管道读进程已终止,写其管道时产生),sigalarm(进程所设置的脑子时钟超时)

可以要求内核在信号发生时采用三种处理方式:忽略信号;捕捉信号,执行预设定的信号处理程序;执行系统默认动作,一般是终止进程。

15 早期的unix系统具备一个特性是:进程在低速的系统调用阻塞期间,如果捕捉到信号,那么系统调用就被中断不再执行。

为了支持这种特性,系统调用被分成,低速的系统调用和其他系统调用。低速的系统调用是指可能会使进程永远阻塞的系统调用。低速的系统调用有,读文件、写文件、打开某种类型的文件、执行ioctl、pause\wait、

进程间通信等。在低速的系统调用中,比较特殊的是和磁盘io有关的系统调用,除非发生了硬件故障,否则读写操作总是能很快的返回,不大可能永久阻塞。

为了方便应用程序的编写,内核提供了中断自动重启机制,应用程序不必处理被中断的系统调用。这类可以自动重启的系统调用包括,ioctl\read\write\wait等。

16 线程包含了表示进程执行环境所需要的所有信息,包括了线程id,一组寄存器值,栈、调度优先级和策略、信号屏蔽字及线程私有变量。进程内所有信息对其线程都是共享的,包含程序正文、程序全局内存和堆内存、文件描述符。

每个线程都有一个线程id,但是这个id只在所属的进程内部有效。

单线程进程,一个进程只会有一个主线程。多线程进程的线程视图则是


线程同步的方式有 互斥量、读写锁(共享-独占锁)、条件变量(条件变量和互斥量一起使用,条件本身由互斥量保护)
 

17 守护进程,也称精灵进程。一般是在系统自举时启动,在系统关闭后退出。他是一种在后台运行生存期比较长的一种进程。

linux系统中,父进程id为0的一般都是系统自举进程,比如像init,bdfulsh(定期冲刷数据到磁盘)等等。

守护进程一般是单例的,为了保证单例。一般会采用文件锁的方式,来保证系统只会启动一个实例。

守护进程的一些实现惯例:

使用文件锁,一般是放在var/run目录下面。

如果支持配置文件,通常放在etc目录下面。

守护进程可以用命令行来启动,但通常他们由系统初始化脚本之一来启动的。

守护进程在启动时会读取配置文件,并不会再次读他。如果配置文件被修改,那么通过异步接收信号的方式SIGHUP重读配置文件。

守护进程一般都用作一个服务器进程,只接收并处理客户请求。

18 记录锁指,当进程在读取或者修改文件的某个区域,他可以禁止其他进程对这个区域进行访问。其实对于内核而言并不存在记录的概念,内核提供的是一种字节范围锁,可以锁定文件的某个字节段。

这种字节锁可以设置为读锁,也可以设置为写锁。

锁的自动继承和释放遵循以下原则:

锁是和文件及进程相关联的,当进程终止时,那么这个进程持有的锁都将释放。当关闭一个文件描述符时,那么进程通过这个描述符所引用的文件上面的锁都将释放。

由fork产生的子进程不继承父进程的文件锁。

在执行exec之后,新程序可以继承原程序上的锁。

19 STREAMS是系统V构造内核设备驱动程序和网络协议包的一种通用方法。流在用户进程和设备驱动程序中间提供了一条全双工的通信信道。流无需和实际的硬件直接会话,也可以用来构造伪设备驱动程序。

任意数量的处理模块都可以压入到流首中,流是基于消息的。

 

20 readv、writev用于在一次函数调用中读写多个非连续缓冲区。也成为散布读(scatter read)和汇聚写(gather write)。当要读写多个非连续缓冲区的时候,需要根据数据量大小来判断是否使用v版读写,大部分情况下使用v版的性能会更好,因为它减少了系统调用的次数,同时也简化程序逻辑,避免用户程序预先合并缓冲区的逻辑。

比如要把两个非连续缓冲区的数据写入到一个文件中有三种方法:调用两次write;先创建新的缓冲区,把两个缓冲区的数据复制到新缓冲区,然后调用一次write;直接调用writev。以下是性能对比。

 

21 存储映射io使磁盘文件和存储空间里的一个缓冲区相互映射。从缓冲区里拿数据相当于从文件里读相应的字节,往缓冲区写数据,相应字节就自动写入文件中。这样就可以在不使用read \write的情况下进行io了。


文件映射的存储空间一般是虚拟内存页大小的倍数,假如文件只有100个字节,而虚拟内存页有512个字节,那么映射区也会有512个字节,剩下的字节被置为0.

调用fork之后子进程会继承父进程的存储映射区。而exec新程序则不继承。

使用文件映射在大部分情况下会提升性能,因为它减少了数据复制的次数。如果采用普通的read\write,数据要先从用户缓冲区复制到内核缓冲区,在从内核缓冲区刷到磁盘。


22 管道是最古老的进程间通信方式之一,基本上所有的unix系统都支持管道。

管道有两个局限性:

它是半双工的,数据只能在一个方向流动。

他们只能在具有公共祖先的进程间使用。

管道是最常见的ipc方式,比如使用shell命令cat temp|grep 'longji' ,shell命令会为两个命令创建进程,并且将第一个进程的标准输出通过管道和第二个进程的标准输入相连接。

23 FIFO,也称为命名管道。和普通管道的区别是他允许非共同祖先的进程进行通信。

它是一种文件,使用FIFO的方式就如同使用文件,要先创建FIFO,类似创建文件的方式,制定文件路径。

创建完之后可以进行read\write\close等操作。一个FIFO可以用于多个进程同时写数据,但是要保证每次写数据不超过PIPE_BUF,否则数据会串掉。

FIFO的两个基本用途:

  • 通过shell命令使用FIFO,使得数据可以从一个管道线传递到另一个管道线,中间无需创建临时文件。
  • FIFO用于客户-服务进程,用于在客户进程和服务进程之间传送数据。

24 有三种IPC被称为XSI IPC,包括消息队列、信号量、共享内存。

每个内核中的IPC结构都是通过一个非负整数的标识符来引用的。要使用这种IPC方式首先要拿到标识符,就如同操作文件要拿到文件描述符一样。但是标识符还是比较特殊的,他可能是一个很大的整数,每次创建都会自增,直到到达最大整数才归0。

XSI IPC有几个重要的问题,例如没有访问计数,当创建他的进程终止时,他不会被自动删除;IPC结构没有文件系统里对应的名字,没有文件描述符,无法使用多路复用技术。

消息队列是消息的链接表,放在内核里用消息队列标识符来标志。本来消息队列是为了提高IPC的性能,但是现在其他类型的IPC(STREAMS、套接字)性能和消息队列没有多大区别,再加上消息队列的各种使用不便,目前已经很少在程序中使用了。

 信号量是一种计数器(含义和jdk里面的信号量一致,用于发放许可,如果只有一个许可,那么成为二元信号量),用于控制多个进程对一个资源的共享访问。

要控制多个进程对一个共享资源的访问可以使用信号量,也可以使用记录锁。

记录锁的使用方式是创建一个空文件,并锁定第一个字节(这个字节可以是空的),分配资源对这个字节加锁,释放资源对这个字节解锁。

虽然使用信号量比记录锁的速度略快,但是从使用的便利性来看,使用记录锁更加不容易出错。当获取字节锁的进程退出时,内核会自动释放这个锁。

共享内存允许多个进程共享一个给定的存储区域,由于这种技术不需要数据复制,所以他的性能是最好的。

使用共享内存要注意共享区域的同步,比如在一个写操作未完成之前不应该允许另外一个进程来读,可以使用信号量或者记录锁来做同步


25 前面讨论的ipc技术都是在同一个操作系统里面进行通信的,当需要跨网络,不同机器之间通信的时候就需要使用套接字接口。套接字接口也同样可以用于单机不同进程间的通信。

套接字是通信端点的抽象,就如同访问文件需要文件描述符,访问套接字也需要套接字描述符。

当多字节的数字在不同机器之间传送时,需要考虑大小端编码的问题。大小端编码和处理器有关,甚至有些处理器可以配置为大端或者小端,情况更加复杂。

还好网络协议可以指定字节序,这样异构计算机系统就可以交换多字节数据而不会产生混淆了。

TCP\IP协议一般采用大端编码。

对于普通的套接字接口,recv方法在没有数据到来时会阻塞,send方法在输出队列没有足够空间来发送数据时也会阻塞。这样会导致消耗过多的线程资源。于是就有了非阻塞模式,select,多路复用。

26 基于streams的管道是一个全双工的管道,单个streams管道就可以向父子进程提供双向数据流。

这种管道可以关联到文件系统中的名字,以此为句柄,多个无关进程之间就可以进行通信。

管道可以用在客户-服务进程的场景,为了避免多个客户同时读写一个服务进程的管道导致数据交错。streams还提供了connld streams模块解决这个问题,可以创建客户-服务之间的唯一连接。

27 unix域套接字提供了单机进程间通信的机制,他可以说是管道和套接字的混合体。他和标准套接字相比,优势在于性能更高,因为它仅仅是复制数据,没有执行协议处理。

28 不同进程打开同一个文件,会产生两个文件描述符,指向不同的文件表项,但是指向同一个v节点。

unix提供了进程之间传送文件描述符的机制,可以通过域套接字或者streams管道传送文件描述符。两个进程就可以共享一个文件描述符,指向同一个文件表项。


29 随着unix的发展,对实现一个多进程多用户访问的数据库系统的支持越来越强。

要实现一个数据库系统,一般可以考虑 :索引文件+数据文件,以此来实现一个按主键的快速查找。

比如索引文件就可以用hash表来存储。通过主键定位到hash的桶,拿到索引记录。索引记录上面包含了实际数据所在的文件、偏移量、长度。由此只要两次io就可以读到数据记录。

下图是一个典型存储设计思路。其中指针存储的都是以ascii数字形式记录的文件中的偏移量。键和索引都以字符串的形式来存储,虽然浪费空间,但是可移植性比较强。


 

数据库系统可以按照集中式或非集中式进行架构,对于并发的控制可以采用文件的字节锁,一般是基于索引文件的字节锁来实现的。



 

 
 


 


 


 
 

猜你喜欢

转载自hill007299.iteye.com/blog/1914588