一、阻塞IO与非阻塞IO
- 当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒
- 非阻塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误
1、阻塞与非阻塞Io读文件
- open()函数打开文件时,为参数 flags 指定 O_NONBLOCK 标志,open()调用成功后,后续的 I/O 操作将以非阻塞式方式进行;这就是非阻塞 I/O 的打开方式,如果未指定O_NONBLOCK 标志,则默认使用阻塞式 I/O进行操作。
- 对于普通文件来说,指定与未指定 O_NONBLOCK 标志对其是没有影响,普通文件的读写操作是不会阻塞的,它总是以非阻塞的方式进行 I/O 操作
- 阻塞读取
* 打开文件 */
fd = open("/dev/input/event3", O_RDONLY);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 读文件 */
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (0 > ret) {
perror("read error");
close(fd);
exit(-1);
}
- 非阻塞读取
/* 打开文件 */
fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 读文件 */
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (0 > ret) {
perror("read error");
close(fd);
exit(-1);
}
- 轮询+非阻塞
/* 打开文件 */
fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 读文件 */
memset(buf, 0, sizeof(buf));
for ( ; ; ) {
ret = read(fd, buf, sizeof(buf));
if (0 < ret) {
printf("成功读取<%d>个字节数据\n", ret);
close(fd);
exit(0);
}
}
2、阻塞IO的优缺点
- 当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式I/O会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞
- 而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮训等待,直到数据可读,要么直接放弃!
- 所以阻塞式I/O的优点在于能够提升 CPU的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU资源,将 CPU 资源让给别人使用
- 而非阻塞式则是抓紧利用CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的CPU 使用率!
3、非阻塞Io并发读取
- 阻塞式读取同时读取两个文件,
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 读鼠标 */
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
/* 读键盘 */
memset(buf, 0, sizeof(buf));
ret = read(0, buf, sizeof(buf));
printf("键盘: 成功读取<%d>个字节数据\n", ret);
就是阻塞式I/O 的一个困境,无法实现并发读取(同时读取)
- 非阻塞式读取两个文件
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将键盘设置为非阻塞方式 */
flag = fcntl(0, F_GETFL); //先获取原来的flag
flag |= O_NONBLOCK; //将O_NONBLOCK标准添加到flag
fcntl(0, F_SETFL, flag); //重新设置flag
for ( ; ; ) {
/* 读鼠标 */
ret = read(fd, buf, sizeof(buf));
if (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
/* 读键盘 */
ret = read(0, buf, sizeof(buf));
if (0 < ret)
printf("键盘: 成功读取<%d>个字节数据\n", ret);
}
将读取鼠标和读取键盘操作放入到一个循环中,通过轮训方式来实现并发读取鼠标和键盘,会占用cpu的利用率很高
二、IO多路复用
- 上面的轮询+非阻塞IO的形式导致cpu的占用率特特别高。
IO多路复用
- I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行I/O 操作时,能够通知应用程序进行相应的读写操作
- I/O多路复用技术是为了解决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。
- I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞I/O
- I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路I/O。
- select与epoll函数
select函数
- 系统调用 select()可用于执行 I/O 多路复用操作,调用 select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)
/*
@ #include <sys/select.h>
@ 文件描述符集合的所有操作都可以通过这四个宏来完成
@ FD_ZERO()将参数 set所指向的集合初始化为空;
@ FD_SET()将文件描述符 fd添加到参数set所指向的集合中;
@ FD_CLR()将文件描述符 fd从参数set所指向的集合中移除;
@ 如果文件描述符fd 是参数set所指向的集合中的成员,则 FD_ISSET()返回 true,否则返回false
*/
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
/*
@ #include <sys/select.h>
@ 参数nfds通常表示最大文件描述符编号值加1
@ readfds是用来检测读是否就绪(是否可读)的文件描述符集合;
@ writefds是用来检测写是否就绪(是否可写)的文件描述符集合;
@ exceptfds是用来检测异常情况是否发生的文件描述符集合。
@ 参数 timeout可用于设定select()阻塞的时间上限,为NULL,表示 select()将会一直阻塞
@ 返回-1表示有错误发生,并且会设置errno
返回 0 表示在任何文件描述符成为就绪态之前 select()调用已经超时
返回一个正整数表示有一个或多个文件描述符已达到就绪态
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//=================example
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#define MOUSE "/dev/input/event3"
int main(void)
{
char buf[100];
int fd, ret = 0, flag;
fd_set rdfds;
int loops = 5;
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将键盘设置为非阻塞方式 */
flag = fcntl(0, F_GETFL); //先获取原来的flag
flag |= O_NONBLOCK; //将O_NONBLOCK标准添加到flag
fcntl(0, F_SETFL, flag); //重新设置flag
/* 同时读取键盘和鼠标 */
while (loops--) {
FD_ZERO(&rdfds);
FD_SET(0, &rdfds); //添加键盘
FD_SET(fd, &rdfds); //添加鼠标
ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
if (0 > ret) {
perror("select error");
goto out;
}
else if (0 == ret) {
fprintf(stderr, "select timeout.\n");
continue;
}
/* 检查键盘是否为就绪态 */
if(FD_ISSET(0, &rdfds)) {
ret = read(0, buf, sizeof(buf));
if (0 < ret)
printf("键盘: 成功读取<%d>个字节数据\n", ret);
}
/* 检查鼠标是否为就绪态 */
if(FD_ISSET(fd, &rdfds)) {
ret = read(fd, buf, sizeof(buf));
if (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
}
}
out:
/* 关闭文件 */
close(fd);
exit(ret);
}
poll函数
系统调用poll()与select()函数很相似
/*
@ fd 是一个文件描述符
@ 调用者初始化 events来指定需要为文件描述符 fd 做检查的事件
@ 当 poll()函数返回时,revents变量由poll()函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件
*/
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
/*
@ #include <poll.h>
@ fds:指向一个struct pollfd类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件,struct pollfd结构体类型
@ nfds:参数 nfds指定了 fds数组中的元素个数,数据类型 nfds_t实际为无符号整形。
@ timeout:该参数与 select()函数的 timeout 参数相似,用于决定 poll()函数的阻塞行为
如果 timeout 等于-1,则poll()会一直阻塞
如果timeout等于0,poll()不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态
如果timeout大于0,则表示设置poll()函数阻塞时间的上限值,直到fds数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止
@ 返回-1表示有错误发生,并且会设置 errno
返回0表示该调用在任意一个文件描述符成为就绪态之前就超时了。
返回一个正整数表示有一个或多个文件描述符处于就绪态了, 返回值表示 fds数组中返回的revents变量不为0的struct pollfd对象的数量
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//=========================example
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#define MOUSE "/dev/input/event3"
int main(void)
{
char buf[100];
int fd, ret = 0, flag;
int loops = 5;
struct pollfd fds[2];
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将键盘设置为非阻塞方式 */
flag = fcntl(0, F_GETFL); //先获取原来的flag
flag |= O_NONBLOCK; //将O_NONBLOCK标准添加到flag
fcntl(0, F_SETFL, flag); //重新设置flag
/* 同时读取键盘和鼠标 */
fds[0].fd = 0;
fds[0].events = POLLIN; //只关心数据可读
fds[0].revents = 0;
fds[1].fd = fd;
fds[1].events = POLLIN; //只关心数据可读
fds[1].revents = 0;
while (loops--) {
ret = poll(fds, 2, -1);
if (0 > ret) {
perror("poll error");
goto out;
}
else if (0 == ret) {
fprintf(stderr, "poll timeout.\n");
continue;
}
/* 检查键盘是否为就绪态 */
if(fds[0].revents & POLLIN) {
ret = read(0, buf, sizeof(buf));
if (0 < ret)
printf("键盘: 成功读取<%d>个字节数据\n", ret);
}
/* 检查鼠标是否为就绪态 */
if(fds[1].revents & POLLIN) {
ret = read(fd, buf, sizeof(buf));
if (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
}
}
out:
/* 关闭文件 */
close(fd);
exit(ret);
}
三、异步IO
- 在I/O多路复用中,进程通过系统调用select()或poll()来主动查询文件描述符上是否可以执行I/O操作
- 而在异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行 I/O操作为止,此时内核会发送信号给进程
- 使用异步IO的步骤:
- 通过指定O_NONBLOCK 标志使能非阻塞I/O。
- 通过指定O_ASYNC 标志使能异步I/O
- 设置异步 I/O 事件的接收进程。
- 为内核发送的通知信号注册一个信号处理函数,默认情况下,异步I/O 的通知信号是SIGIO
- 以上步骤完成之后,进程就可以执行其它任务了
当 I/O 操作就绪时,内核会向进程发送一个SIGIO信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数进行I/O操作。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#define MOUSE "/dev/input/event3"
static int fd;
static void sigio_handler(int sig)
{
static int loops = 5;
char buf[100] = {0};
int ret;
if(SIGIO != sig)
return;
ret = read(fd, buf, sizeof(buf));
if (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
loops--;
if (0 >= loops) {
close(fd);
exit(0);
}
}
int main(void)
{
int flag;
/* 打开鼠标设备文件<使能非阻塞 I/O> */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 使能异步I/O */
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
/* 设置异步I/O 的所有者 */
fcntl(fd, F_SETOWN, getpid());
/* 为SIGIO 信号注册信号处理函数 */
signal(SIGIO, sigio_handler);
for ( ; ; )
sleep(1);
}
- 当需要检查大量文件描述符时,可以使用epoll解决 select()或 poll()性能低的问题
四、存储映射IO
- 存储映射I/O(memory-mapped I/O)是一种基于内存区域的高级I/O操作,它能将一个文件映射到进程地址空间中的一块内存区域中
- 当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行write 操作)。
- 这样就可以在不使用基本 I/O操作函数read()和write()的情况下执行 I/O操作。因为read和write函数再读写文件的过程中存在缓存的拷贝
1、mmap与munmap
/*===================存储映射
@ #include <sys/mman.h>
@ addr:参数 addr 用于指定映射到内存区域的起始地址
@ length:参数length指定映射长度,表示将文件中的多大部分映射到内存区域中,以字节为单位
@ offset: 文件映射的偏移量,通常将其设置为0,表示从文件头部开始映射
@ fd:文件描述符,指定要映射到内存区域中的文件。
@ prot:参数 prot指定了映射区的保护要求,通过按位或运算符任意组合
PROT_EXEC:映射区可执行;
PROT_READ:映射区可读;
PROT_WRITE:映射区可写;
PROT_NONE:映射区不可访问。
@ flags:参数 flags可影响映射区的多种属性,
MAP_SHARED:当对映射区写入数据时,数据会写入到文件中,并且允许其它进程共享。
MAP_PRIVATE:对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写
MAP_FIXED:MAP_ANONYMOUS:建立匿名映射,此时会忽略参数 fd 和 offset,不涉及文件,而且映射区域无法和其它进程共享。
MAP_LOCKED:对映射区域进行上锁
@ 返回值:成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1,
*/
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*=========================解除存储映射
@ #include <sys/mman.h>
@ 参数 addr指定待解除映射地址范围的起始地址,它必
须是系统页大小的整数倍
@ 参数 length是一个非负整数,指定了待解除映射区域的大小
@
@ 需要注意的是,当进程终止时也会自动解除映射(如果程序中没有显式调用munmap()),但调用close()关闭文件时并不会解除映射。
*/
int munmap(void *addr, size_t length)
//==========================exmaple使用存储映射 I/O复制文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
int main(int argc,char *argv[])
{
int srcfd,dstfd;
if(3!=argc){
fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv[0]);
exit(-1);
}
/*打开源文件*/
srcfd=open(argv[1].O_RDONLY);
if (-1 == srcfd) {
perror("open error");
exit(-1);
}
/*打开目标文件*/
dstfd=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664);
if (-1 == dstfd) {
perror("open error");
ret = -1;
goto out1;
}
/*获取源文件的大小*/
fstat(srcfd,&sbuf);
/*设置目标文件的大小*/
ftruncate(dstfd,sbu.st_size);
/*将源文件映射到内存区域中*/
srcaddr=mmap(NULL.sbuf.st_size,PROT_READ,MAP_SHARED,srcfd,0);
if(MAP_FAILED==srcaddr){
perror("mmap error");
ret = -1;
goto out2;
}
/*将目标文件映射到内存区域中*/
dstaddr=mmap(NULL,sbuf.st_size,PROT_WRITE,MAP_SHARED,dstfd,0);
if (MAP_FAILED == dstaddr) {
perror("mmap error");
ret = -1;
goto out3;
}
/*将源文件中的内容复制到目标文件中*/
memcpy(dstaddr,srcaddr,sbuf.st_size);
/* 程序退出前清理工作 */
out4:
/* 解除目标文件映射 */
munmap(dstaddr, sbuf.st_size);
out3:
/* 解除源文件映射 */
munmap(srcaddr, sbuf.st_size);
out2:
/* 关闭目标文件 */
close(dstfd);
out1:
/* 关闭源文件并退出 */
close(srcfd);
exit(ret);
}
2、普通IO与存储映射IO比较
- 普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写,使用 read()和 write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的
缓存间倒腾,效率会比较低
- 存储映射I/O 的实质其实是共享,与IPC 之内存共享很相似。譬如执行一个文件复制操作来说,对于普通 I/O方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中
- 对比:
- 使用存储映射 I/O 减少了数据的复制操作,所以在效率上会比普通 I/O 要高,
- 应用层和内核层时不能直接交互的,必须需要系统调用来做桥梁实现交互,可以认为映射区就是应用层与内核层中间共享内存
- 存储映射IO适用于,进行大数据操作时候比较有效,而对于少量数据,使用普通IO方式更加方便
五、文件加锁
- 多个进程同时操作同一文件,很容易导致文件中的数据发生混乱,因为多个进程对文件进行 I/O操作时,容易产生竞争状态、导致文件中的内容与预想的不一致!
- 对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行I/O操作,在这段时间内不允许其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux系统提供了文件锁机制
- 在多线程环境下,互斥锁、自旋锁、读写锁机制实现用于对共享资源的访问进行保护,
- 一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,归根结底,文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争状态。
1、文件锁分类
-
文件锁可以分为建议性锁和强制性锁
-
建议性锁:
- 建议性锁本质上是一种协议,程序访问文件之前,先对文件上锁,上锁成功之后再访问文件,这是建议性锁的一种用法;
- 但是如果你的程序不管三七二十一,在没有对文件上锁的情况下直接访问文件,也是可以访问的,并非无法访问文件如果是这样,那么建议性锁就没有起到任何作用
-
强制性锁:
- 如果进程对文件上了强制性锁,其它的进程在没有获取
到文件锁的情况下是无法对文件进行访问的 - 锁会让内核检查每一个 I/O 操作(譬如 read()、write()),验证调用进程是否是该文件锁的拥有者,如果不是将无法访问文件。当一个文件被上锁进行写入操作的时候,内核将阻止其它进程对其进行读写操作
- 采取强制性锁对性能的影响很大,每次进行读写操作都必须检查文件锁。
- 如果进程对文件上了强制性锁,其它的进程在没有获取
2、文件锁操作
- flock()、fcntl()以及 lockf()这三个函数对文件上锁
- flock函数文件加锁
/*
@ #include <sys/file.h>
@ fd:参数fd为文件描述符,指定需要加锁的文件
@ operation:参数operation指定了操作方式
LOCK_SH:在 fd 引用的文件上放置一把共享锁
LOCK_EX:在 fd 引用的文件上放置一把排它锁(或叫互斥锁)
LOCK_UN:解除文件锁定状态,解锁、释放锁。
LOCK_NB:表示以非阻塞方式获取锁
@ 返回值:成功将返回0;失败返回-1、并会设置errno,
@ 对于flock(),需要注意的是,同一个文件不会同时具有共享锁和互斥锁
*/
int flock(int fd, int operation);
//==========example使用flock对文件加锁/解锁
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h
static int fd=-1;
/*信号处理函数*/
static void sigint_handler(itn sig)
{
if(SIGINT!=sig)
return;
/*解锁*/
flock(fd,LOCK_UN);
close(fd);
printf("进程1:文件已经解锁!\n");
}
int main(int argc,char *argv[])
{
if(2!=argc){
fprint(stderr,"usage:%s<file>\n",argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_WRONLY);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 以非阻塞方式对文件加锁(排它锁) */
if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
perror("进程1: 文件加锁失败");
exit(-1);
}
printf("进程1: 文件加锁成功!\n");
/* 为SIGINT信号注册处理函数 */
signal(SIGINT, sigint_handler);
for(;;)
sleep(1);
}
//====另开一个进程,未获取锁情况下读写文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>
int main(int argc, char *argv[])
{
char buf[100] = "Hello World!";
int fd;
int len;
if (2 != argc) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 以非阻塞方式对文件加锁(排它锁) */
if (-1 == flock(fd, LOCK_EX | LOCK_NB))
perror("进程2: 文件加锁失败");
else
printf("进程2: 文件加锁成功!\n");
/* 写文件 ,读写文件时会该变offset的位置 */
len = strlen(buf);
if (0 > write(fd, buf, len)) {
perror("write error");
exit(-1);
}
printf("进程2: 写入到文件的字符串<%s>\n", buf);
/* 将文件读写位置移动到文件头 */
if (0 > lseek(fd, 0x0, SEEK_SET)) {
perror("lseek error");
exit(-1);
}
/* 读文件 */
memset(buf, 0x0, sizeof(buf)); //清理buf
if (0 > read(fd, buf, len)) {
perror("read error");
exit(-1);
}
printf("进程2: 从文件读取的字符串<%s>\n", buf);
/* 解锁、退出 */
flock(fd, LOCK_UN);
close(fd);
exit(0);
}
/*
@ flock的几条准则:
同一进程对文件多次加锁不会导致死锁
文件关闭的时候,会自动解锁。
一个进程不可以对另一个进程持有的文件锁进行解锁。
由 fork()创建的子进程不会继承父进程所创建的锁
*/
- fcntl()函数加锁
/*
@ #include <unistd.h>
#include <fcntl.h>
@ lock()仅支持对整个文件进行加锁/解锁;而 fcntl()可以对文件的某个区域(某部分内容)进行加锁/解锁,可以精确到某一个字节数据。
@ flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型。
*/
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );