系统调用
所谓系统调用是指操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务。例如用户可以通过进程控制相关的系统调用来创建进程、实现进程调度、进程管理等。
为什么用户程序不能直接访问系统内核提供的服务呢?这是由于在Linux中,为了更好地保护内核空间,将程序的运行空间分为内核空间和用户空间(也就是常称的内核态和用户态),它们分别运行在不同的级别上,在逻辑上是相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间的函数。
但是,在有些情况下,用户空间的进程需要获得一定的系统服务(调用内核空间程序),这时操作系统就必须利用系统提供给用户的“特殊接口”——系统调用规定用户进程进入内核空间的具体位置。进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完后再返回到用户空间。
API
前面讲到的系统调用并不是直接与程序员进行交互的,它仅仅是一个通过软中断机制向内核提交请求,以获取内核服务的接口。在实际使用中程序员调用的通常是用户编程接口——API
系统命令相对API更高了一层,它实际上一个可执行程序,它的内部引用了用户编程接口(API)来实现相应的功能。
6.2 Linux中文件及文件描述符概述
内核如何区分和引用特定的文件呢?这里用到了一个重要的概念——文件描述符。对于Linux而言,所有对设备和文件的操作都是使用文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,并指向在内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
通常,一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。这3个文件分别对应文件描述符为0、1和2(也就是宏替换STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO)。
6.3 底层文件I/O操作
函数说明
open()函数是用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
close()函数是用于关闭一个被打开的文件。当一个进程终止时,所有被它打开的文件都由内核自动关闭,很多程序都使用这一功能而不显示地关闭一个文件。
read()函数是用于将从指定的文件描述符中读出的数据放到缓存区中,并返回实际读入的字节数。若返回0,则表示没有数据可读,即已达到文件尾。读操作从文件的当前指针位置开始。当从终端设备文件中读出数据时,通常一次最多读一行。
write()函数是用于向打开的文件写数据,写操作从文件的当前指针位置开始。对磁盘文件进行写操作,若磁盘已满或超出该文件的长度,则write()函数返回失败。
lseek()函数是用于在指定的文件描述符中将文件指针定位到相应的位置。它只能用在可定位(可随机访问)文件操作中。管道、套接字和大部分字符设备文件是不可定位的,所以在这些文件的操作中无法使用lseek()调用。
文件锁
文件锁包括建议性锁和强制性锁。建议性锁要求每个上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。在一般情况下,内核和系统都不使用建议性锁。强制性锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他任何文件对其进行读写操作。采用强制性锁对性能的影响很大,每次读写操作都必须检查是否有锁存在。
在Linux中,实现文件上锁的函数有lockf()和fcntl(),其中lockf()用于对文件施加建议性锁,而fcntl()不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl()还能对文件的某一记录上锁,也就是记录锁。
记录锁又可分为读取锁和写入锁,其中读取锁又称为共享锁,它能够使多个进程都能在文件的同一部分建立读取锁。而写入锁又称为排斥锁,在任何时刻只能有一个进程在文件的某个部分上建立写入锁。当然,在文件的同一部分不能同时建立读取锁和写入锁。
fcntl()函数格式(1)
fcntl()函数格式(2)- flock结构
struct flock
{
short l_type;
off_t l_start;
short l_whence;
off_t l_len;
pid_t l_pid;
}
6.5 标准I/O编程
标准I/O编程概述
前面讲述的系统调用是操作系统直接提供的函数接口,也都是基于文件文件描述符的,现在我们讲解基于流缓冲的文件IO操作。因为运行系统调用时,Linux必须从用户态切换到内核态,执行相应的请求,然后再返回到用户态,所以应该尽量减少系统调用的次数,从而提高程序的效率。标准I/O提供流缓冲的目的是尽可能减少使用read()和write()等系统调用的数量。标准I/O提供了3种类型的缓冲存储。
· 全缓冲:在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。对于存放在磁盘上的文件通常是由标准I/O库实施全缓冲的。
· 行缓冲:在这种情况下,当在输入和输出中遇到行结束符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(如fputc()函数),但只有写了一行之后才进行实际I/O操作。标准输入和标准输出就是使用行缓冲的典型例子。
· 不带缓冲:标准I/O库不对字符进行缓冲。如果用标准I/O函数写若干字符到不带缓冲的流中,则相当于用系统调用write()函数将这些字符全写到被打开的文件上。标准出错stderr通常是不带缓存的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个行结束符。
基本操作 (1)
打开文件:
有三个标准函数,分别为:fopen()、fdopen()和freopen()。它们可以以不同的模式打开,但都返回一个指向FILE的指针,该指针指向对应的I/O流。此后,对文件的读写都是通过这个FILE指针来进行。其中fopen()可以指定打开文件的路径和模式,fdopen()可以指定打开的文件描述符和模式,而freopen()除可指定打开的文件、模式外,还可指定特定的I/O流。
基本操作 (2)
其中,mode定义打开文件的访问权限等:
基本操作 (3)
关闭文件:
关闭标准流文件的函数为fclose(),该函数将缓冲区内的数据全部写入到文件中,并释放系统所提供的文件资源。
基本操作 (4)
在文件流被打开之后,可对文件流进行读写等操作,其中读操作的函数为fread() :
基本操作 (5)
fwrite()函数是用于对指定的文件流进行写操作。
复习一下前面的知识--其它相关操作(1)
字符输入函数
字符输出函数
其它操作(2)
行输入函数
行输出函数
其它操作(3)-格式化输入输出(1)
其它操作(4)-格式化输入输出(2)
- mmap()函数
- 函数mmap()将某个文件的指定内容映射到内容内存空间中,从而提供不同于一般的普通文件访问方式,进程可以像读写内存一样对普通文件的操作,因为内存比磁盘速度要快,从而加快了访问速度。
- start 参数
- 如果start 参数为NULL,内核会自己在进程地址空间中选择合适的地址建立映射。
- 如果start 不为NULL,则给内核一个提示,应该从什么地址开始映射,内核会选择start 之上的某个合适的地址开始映射。
- 建立映射后,真正的映射首地址通过返回值可以得到
- length 参数是需要映射的那一部分文件的长度。
- offset参数是从文件的什么位置开始映射
- 必须是页大小的整数倍(在32 位体系统结构上通常是4K)
- 3. mmap()
- fd 是代表该文件的描述符
- prot 参数有四种取值:
- PROT_EXEC 表示映射的这一段可执行,例如映射共享库
- PROT_READ 表示映射的这一段可读
- PROT_WRITE 表示映射的这一段可写
- PROT_NONE 表示映射的这一段不可访问
- flag 参数有很多种取值 ---- 查看mmap(2)
- MAP_SHARED 多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化。
- MAP_PRIVATE 多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去。
- 其它取值可当进程终止时,该进程的映射内存会自动解除,也可以调用munmap()解除映射
- 返回值
- 如果mmap()成功则返回映射首地址,如果出错则返回常数MAP_FAILEDØ
- munmap()成功返回0,出错返回-1 。
- 实验1 -文件读写及上锁(1)
- 1.实验目的
- 通过编写文件读写及上锁的程序,进一步熟悉Linux中文件I/O相关的应用开发,并且熟练掌握open()、read()、write()、fcntl()等函数的使用。
- 2.实验内容
- 在Linux中FIFO是一种进程之间的管道通信机制。Linux支持完整的FIFO通信机制。
- 我们通过使用文件操作,仿真FIFO(先进先出)结构以及生产者-消费者运行模型。
- 本实验中需要打开两个虚拟终端,分别运行生产者程序(producer)和消费者程序(customer)。此时两个进程同时对同一个文件进行读写操作。因为这个文件是临界资源,所以可以使用文件锁机制来保证两个进程对文件的访问都是原子操作。