进程是一个具有一定独立功能的程序关于某个数据集的一次运行活动。每个进程都有自己独立的进程地址空间,包括正文段、初始化数据段、非初始化数据段、栈、堆。文本区域存储将要被执行的代码,数据区域存储变量,栈存储自动变量以及每次函数调用时所需保存的信息,堆用于动态分配内存。
进程是操作系统执行的基本单元,对操作系统的各种资源进行访问,而进程对文件的访问尤为频繁。文件是具有符号的,在逻辑上具有完整意义的一组相关信息项的有序序列。这种信息的持久性是由文件系统来维护的。所以进程对文件的访问实际上是通过访问文件系统实现的。
UNIX文件系统具有类似的通用结构:superblock、inode、datablock。superblock包括文件系统的总体信息,inode包括一个文件的所有信息,datablock用于具体的存储文件数据。一般来说文件系统都会维护一个(或数个)inodetable用于分配回收文件,维护一个(或数个)datatable用于分配回收数据块。
一般来讲对文件系统的操作会涉及以下过程:新建一个文件并向其中写入一定数据时,文件系统需要从inodetable中分配出一个空闲的inode并减少空闲inode个数,向文件中写数据时,文件系统需要从datatable中分配出空闲的数据块并减少空闲数据块个数。当然文件系统需要做一些其他的必要操作,如把该文件的文件名加入父目录的数据块,该文件每新增一个数据块修改该inode的数据块个数,访问时间等。删除一个文件时,文件系统的操作相反。
考虑以下情况:系统中有两个进程(P1、P2)都需要新建一个文件,进程P1要求从inodetable中分配一个inode,并把空闲inode个数减1,但这个过程需要数个时钟周期,若在进程P1获得当前空闲文件个数后其即被调度,这时进程P2开始执行,并成功在文件系统中新建一个文件,这时进程P1恢复运行并完成后续工作。很明显这个过程中文件系统新建了两个文件,但是空闲inode个数却只减少了一个。空闲数据块的分配也存在同样的问题。所以当两个或两个以上进程同时修改文件系统信息时就需要一定的同步机制,来保障文件系统的正确性。
为了支持多个进程同时修改文件系统,传统的文件系统在分配空闲inode和datablock代码段加上互斥锁,来保证分配空闲inode和datablock的原子性。这样只有一个进程完成分配inode或datablock整个过程后,另一个进程才会被允许进行相关操作。但这并不是说文件系统已经完美的支持了多进程,当多个进程同时修改一个文件时,就会出现新的问题。
再考虑以下情况:系统中有两个进程(P1、P2)需要同时向文件F中写入一个数据块大小的数据,进程P1要求从分得一个空闲数据块,并把文件F的inode数据块加1,,但这个过程同样需要数个时钟周期,若在进程P1获得当前文件数据块个数后被调度,进程P2开始执行,并成功向文件F中写入数据,这时进程P1恢复运行并完成后续工作。很明显这个过程中文件F新增了两个数据块,但是文件F中记录的新增数据块个数却只增加了1个。所以当两个或两个以上进程同时修改文件信息时也需要一定的同步机制,来保障文件系统带的正确性。
为了支持多个进程同时修改文件信息,当然可以像管理空闲inode和datablock一样在相应的代码段加上互斥锁,来保证修改文件信息的原子性。但是UNIX操作系统却并没有这么做,而是给出了针对文件系统单独设计了文件锁和记录锁。每次对文件进行访问时,文件系统都会在VFS层检验是否加锁(文件锁和记录锁的详细介绍和使用方法在很多文档中都有描述,这里不再给出)。
多个进程同时访问同一个文件,在访问数据前加上互斥锁,完成后释放互斥锁,完全可以保证对文件操作的原子性,而且这样做的开销要小于单独实现的文件锁。显然UNIX操作系统放弃这种表面上看似完美设计的原因就是因为记录锁了。在真实环境中,两个或两个以上进程同时访问一个文件的概率很高,但是一般情况下它们只是对文件的一部分进行操作。为了访问文件的一部分而对整个文件进行加锁,以保证对文件操作的原子性显然是不合适的。
在数据库操作中,大多数进程对文件访问都只是一条记录,这时如果把整个文件都锁起来,其他需要访问该文件的进程就不得不等待该进程访问结束后才能对该文件进行操作,这样严重限制了对文件操作的并发性。而如果能够在对文件访问时,准确的对访问部分进行加锁,文件其他部分依然能够被别的进程*访问,情况就明显不一样了。所以相对于能够对文件进行并发访问,单独实现文件锁和记录锁在单次访问时增加的系统开销就显得合情合理了,而且现在CPU的高性能使得这样做的理由更加充分。
有上述可知,多进程的并发性可以分为,对文件系统访问的并发性和对文件访问的并发性。为了支持对文件系统的并发访问,文件系统需要在多进程访问会修改到的公共资源处加上互斥锁;为了支持对文件的并发访问,UNIX操作系统提供了文件锁和记录锁。