非阻塞IO基本概念
高级IO核心就一个概念:非阻塞IO。
与该概念相对的,就是我们之前学习过的阻塞IO。
非阻塞IO(Non-blocking I/O)是一种IO模型,用于实现异步IO操作,使应用程序能够在等待IO操作完成的同时继续执行其他任务。
非阻塞IO主要解决的是阻塞IO模型中,当数据未准备好或设备不可用时,应用程序会挂起等待的问题。在非阻塞IO模型中,当数据未准备好或设备不可用时,应用程序会立即返回一个错误,而不是挂起等待。这样,应用程序可以继续执行其他任务,而不是等待IO操作完成。
需要注意的是,非阻塞IO并不意味着IO操作会立即完成,而是意味着应用程序可以在等待IO操作完成的同时继续执行其他任务。非阻塞IO的实现需要系统内核的支持,通常需要使用特殊的系统调用或者设备驱动程序来完成。
因此,非阻塞IO是一种更适合于高并发、高性能、实时性要求较高的应用的IO模型。它可以提高应用程序的响应速度和并发性能,同时减少应用程序的等待时间和资源浪费。
补充:有限状态机编程思想
首先要说明一个数据中继的术语,通俗的说就是在两个设备(也可以说文件亦或者是两个单位)之间,一个设备的数据拿到另一个设备去,然后数据在这个设备被加工计算之后又送回原来的设备上。
对于这种数据中继模式,从任务的角度看,假设下图中的左圈和右圈分别表示两个设备:
那么我们要做的事情是不是读左写右,然后又读右写左。
如果用我们之前的阻塞IO的模式,那么是不是很有可能会被阻塞在某一个部分就迟迟推进不了(要么阻塞在写要么阻塞在读),这效率就很低,而且一般都是一个线程(或者说进程)一直在操作,没有多线程协同。
我们完全可以分成多任务的形式,比如采用两个线程,一个来进行读左写右的任务,一个来进行读右写左的任务,这样哪一端有数据那么哪一端就可以先进行操作了。
这样好像比之前的单任务模式已经强很多了,但还是不够,试想万一多任务形式中的两个线程同时都阻塞了那不也还是很难往下推进了吗?
所以非阻塞IO就横空出世了,这是现代服务器能够实现高并发、高性能的根本!
有限状态机:
有限状态机(Finite State Machine,FSM)是一种用来进行对象行为建模的工具,它用于描述对象在其生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。在计算机中,有限状态机被广泛应用于建模应用行为、硬件电路系统设置、软件工程、编译器、网络协议和计算与语言的研究等领域。
有限状态机模型由一组状态和一组转移条件组成。每个状态代表了对象的一个特定状态,而转移条件则描述了当某个事件发生时,对象从一个状态转移到另一个状态的条件。在有限状态机中,对象的行为被建模为从一个状态转移到另一个状态的过程,其中每个状态都是一个完整的、独立的实体。
在Linux中,有限状态机被广泛用于各种不同的领域,如网络协议、设备驱动程序、文件系统等。例如,网络协议中的TCP/IP连接就是通过有限状态机来实现的。有限状态机可以帮助我们理解和设计复杂的系统,使系统中的每个状态和转移都更加清晰和易于管理。
接下来我们将用有限状态机来实现上面的数据中继问题,用两个终端来模拟。
非阻塞IO实例
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
//这是两个终端
#define TTY1 "/dev/tty11"
#define TTY2 "/dev/tty12"
#define BUFSIZE 1024
//枚举状态类
enum{
STATE_R = 1, //读态
STATE_W, //写态
STATE_Ex, //异常态
STATE_T //T态
};
//状态机fsm的数据结构
struct fsm_st{
int state; //状态
int sfd; //源文件
int dfd; //目标文件
char buf[BUFSIZE]; //数据缓冲区
int len; //读取长度
char* errstr; //报错内容
int pos; //用来让读取缓冲区更加彻底用的
};
//推状态机前进
static void fsm_driver(struct fsm_st* fsm){
int ret;
switch(fsm->state){
case STATE_R:
fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE);
if(fsm->len == 0){
fsm->state = STATE_T;
}else if(fsm->len < 0){
if(errno == EAGAIN){//只是因为被阻塞造成的假错,那么就继续读
fsm->state = STATE_R;
}else{
fsm->errstr = "read()";
fsm->state = STATE_Ex;
}
}else{
fsm->pos = 0;
fsm->state = STATE_W;
}
break;
case STATE_W:
ret = write(fsm->dfd,fsm->buf+fsm->pos,fsm->len);
if(ret < 0){
if(errno == EAGAIN){
fsm->state = STATE_W;
}else{
fsm->errstr = "write()";
fsm->state = STATE_Ex;
}
}else{
fsm->pos += ret;
fsm->len -= ret;
if(fsm->len == 0){
fsm->state = STATE_R;
}else{
fsm->state = STATE_W;
}
}
break;
case STATE_Ex:
perror(fsm->errstr);
fsm->state = STATE_T;
break;
case STATE_T:
/*do sth*/
break;
default:
abort();//进程异常退出
break;
}
}
//用fd1 和fd2 来模拟两个设备
//也就是数据中继的模型
static void relay(int fd1,int fd2){
int fd1_save,fd2_save;
//定义俩状态机,fsm12读左写右,fsm21读右写左
struct fsm_st fsm12,fsm21;
fd1_save = fcntl(fd1,F_GETFL);
fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK);
fd2_save = fcntl(fd2,F_SETFL);
fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK);
fsm12.state = STATE_R;
fsm12.sfd = fd1;
fsm12.dfd = fd2;
fsm21.state = STATE_R;
fsm21.sfd = fd2;
fsm21.dfd = fd1;
while(fsm12.state != STATE_T || fsm21.state != STATE_T){
fsm_driver(&fsm12);
fsm_driver(&fsm21);
}
fcntl(fd1,F_SETFL,fd1_save);
fcntl(fd2,F_SETFL,fd2_save);
}
int main(){
int fd1,fd2;
fd1 = open(TTY1,O_RDWR);
if(fd1 < 0){
perror("open()");
exit(1);
}
write(fd1,"TTY\n",5);
fd2 = open(TTY2,O_RDWR|O_NONBLOCK);
if(fd2 < 0){
perror("open()");
exit(1);
}
write(fd2,"TTY2\n",5);
//调用中继引擎函数
relay(fd1,fd2);
close(fd2);
close(fd1);
exit(0);
}
我目前的水平也不太懂这些操作,先放在这里吧,以后应该就懂了。
IO多路复用(IO多路转接)
select
在Linux中,IO多路复用是一种处理多个文件描述符(sockets、pipes、files等)输入/输出(I/O)请求的技术。它允许一个进程同时监控多个文件描述符的状态,以便在它们准备好进行读或写操作时进行相应的操作。
IO多路复用技术主要解决了传统轮询方式(如select和poll)的不足,提高了处理大量并发连接的效率。它通过使用事件驱动机制,只在有事件发生时才通知应用程序,从而减少了无效的轮询和等待时间。
在Linux中,常见的IO多路复用技术包括select、poll和epoll。select函数原型如下:
c
#include <sys/select.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
其中,readset、writeset和exceptset分别表示可读、可写和异常的文件描述符集合,maxfd表示这些描述符集合中的最大值加1,timeout表示等待的超时时间。select函数会监控这些描述符集合中的状态变化,当有描述符的事件发生时,select函数就会返回。
对于fd_set文件描述符集合,有以下函数常用:
在C语言中,与文件描述符集合fd_set相关的函数主要包括以下三个:
FD_SET(fd_set *fdset): 这个宏用于在文件描述符集合中增加一个新的文件描述符。fdset是一个指向fd_set类型的指针,它表示要操作的集合。参数fd是要添加到集合中的文件描述符。
FD_CLR(fd_set *fdset): 这个宏用于在文件描述符集合中删除一个文件描述符。fdset是一个指向fd_set类型的指针,它表示要操作的集合。参数fd是要从集合中删除的文件描述符。
FD_ISSET(int fd, fd_set *fdset): 这个宏用于测试指定的文件描述符是否在该集合中。参数fd是要检查的文件描述符,fdset是一个指向fd_set类型的指针,它表示要检查的集合。如果文件描述符fd在集合fdset中,则返回非零值;否则,返回0。
FD_ZERO(fd_set *fdset):这个函数用于清空文件描述符集合fdset。在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。因此,在使用FD_SET添加文件描述符之前,通常会先使用FD_ZERO来清空集合。
这些函数都是通过fd_set数据类型来进行操作的,而fd_set实际上是一个整数类型的数组,用于存储文件描述符。在UNIX系统中,有一个常量FD_SETSIZE定义了fd_set的最大容量,通常这个值是1024。
改造之前的程序,将其用select进行改写(改写关键部分,其余部分未变,主要体会这套标准用法:布置监视任务、监视、查看监视结果这一套流程,后面的poll和epoll都是这个框架),别细究代码。主要看逻辑框架和注释部分的内容以及select的使用,不懂的可以去看李慧芹老师的课嗷:
select的缺陷主要包括以下几点:
描述符数量的限制:select的描述符数量默认是1024,这对于高并发的场景来说远远不够。如果需要同时监听更多的文件描述符,就需要使用更大的数组,这会增加内存的开销。
每次调用select之前都需要依次向fd_set中加入每个待监听的描述符(注意是每次嗷,因为其监听现场和监视结果是存放在同一块内存空间的,所以每次调用前都得依次向集合中添加待监听的文件描述符),这会增加编程的复杂度和降低效率。
调用select时,需要将fd_set从用户态copy到内核,超时时,又需要将返回的fd_set从内核copy到用户态。这个过程中涉及数据在不同内存空间之间的拷贝,会带来一定的性能开销。
select底层是通过轮询每个描述符来判断是否有描述符就绪的,这种轮询方式可能导致效率低下。
select返回的结果只是就绪的描述符的个数,而不是具体事件,因此需要遍历描述符,依次判断每个描述符是否就绪,这会增加程序的复杂度。
select不支持文件描述符的重用。每次内核检测到事件都会修改fd_set,所以每次都需要重置fd_set。这可能导致在大量并发连接或频繁的事件触发下,系统资源的浪费和效率降低。
select不支持对非标准I/O的设备进行监听。例如,select无法监听管道、命名管道、信号量、共享内存等非标准I/O设备,这限制了select的应用范围。
因此,虽然select有一定的应用场景,但在面对大规模并发连接和高性能要求时,其缺陷可能会成为系统性能的瓶颈。在这种情况下,可以考虑使用其他IO多路复用技术,如epoll等。
poll
在Linux中,poll()函数是一个通用的I/O多路复用方法,可以用于检查文件描述符的状态,包括可读、可写和异常状态。其函数原型为:
c
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
其中,pollfd是一个结构体类型,用于描述需要监视的文件描述符及其状态。其定义如下:
c
struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events that occurred */
};
fd:用于指定需要监视的文件描述符。
events:用于指定需要监视的事件类型。例如,POLLIN表示监视可读事件,POLLOUT表示监视可写事件,POLLERR表示监视异常事件等。可以使用位运算将这些事件组合起来。
revents:用于存储发生的事件类型。在调用poll()函数后,这个值会被更新。
在调用poll()函数时,第一个参数fds是一个指向struct pollfd类型的指针,指向一个数组,该数组中的每个元素都描述了一个需要监视的文件描述符及其状态。nfds参数指定了这个数组的大小,即需要监视的文件描述符的数量。如果文件描述符的数量超过了数组的大小,就会出现错误。
最后,timeout参数是一个整数,表示等待的最长时间(单位为毫秒),如果设置为-1,poll就会一直等待,直到有事件发生。如果设置为0,poll则不会等待,而是立即返回。
还是之前的程序,只改关键部分:
poll函数的优点主要包括:
不要求开发者计算最大文件描述符加一的大小,相比select函数,poll函数在应付大数目的文件描述符时速度更快。
没有最大连接数的限制,原因是它是基于链表来存储的。
poll函数的缺点主要包括:
存在大量fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是否有意义。
与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
epoll
epoll函数原型如下:
c
#include <sys/epoll.h>
int epoll_wait(int epoll_fd, struct epoll_event *events, int max_events, int timeout);
其中,epoll_fd是一个事件驱动的文件描述符,events是一个存储事件的结构体数组,max_events表示数组的大小,timeout表示等待的超时时间。epoll函数会监控epoll_fd中文件描述符的状态变化,当有文件描述符的事件发生时,epoll函数就会返回。与select和poll不同,epoll使用了事件驱动机制,可以更高效地处理大量并发连接。
epoll_wait是epoll机制中的一个函数。除此之外,epoll机制还包含另外两个基本的函数:epoll_create和epoll_ctl。
epoll机制体系下的相关函数详解
epoll_create函数:
c
#include <sys/epoll.h>
int epoll_create(int size);
函数语义:用于创建一个epoll对象(epoll instance),同时返回该对象的描述符。参数size是告诉内核需要监听的文件描述符的最大数量,但只是给内核一个提示,实际上在Linux 2.6.8版本以后这个参数已经被忽略,只需要传入一个大于0的值即可。返回值:指向新创建的epoll对象的文件描述符,如果创建失败则返回-1。
epoll_ctl函数:
c
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数语义:操作epoll对象,向其中添加、修改或删除文件描述符。参数epfd是epoll_create函数的返回值,表示epoll对象的文件描述符。参数op表示要执行的操作,可以是以下三个值之一:EPOLL_CTL_ADD(添加文件描述符到epoll对象)、EPOLL_CTL_MOD(修改文件描述符在epoll对象中的事件)和EPOLL_CTL_DEL(从epoll对象中删除文件描述符)。参数fd表示要操作的文件描述符。参数event是一个结构体指针,用于描述文件描述符及其事件。返回值:成功返回0,失败返回-1。
epoll_wait函数:
c
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数语义:等待注册在epoll对象上的文件描述符的事件发生。参数epfd是epoll对象的文件描述符。参数events是一个结构体数组,用于存储发生事件的文件描述符的信息。参数maxevents表示数组的最大元素个数。参数timeout表示等待的超时时间,单位是毫秒。如果timeout为-1,则表示永久等待,直到有事件发生;如果timeout为0,则表示不等待,立即返回;如果timeout大于0,则表示等待指定的超时时间。返回值:表示满足监听条件的文件描述符的总个数,如果超时或出错则返回-1。
还是以修改之前程序的关键代码为例示范:
感觉这个例子解释的epoll函数不是很清晰...就当了解一下吧,后面再去看其它的内容来补充,知道它是干什么用的就可以了。
其它读写函数
readv()
readv函数是Linux系统中的一个I/O操作函数,它允许用户程序在一次系统调用中从文件描述符读取多个分散的数据块。这个函数特别适合于需要从文件或其他I/O设备中读取不同大小的数据块的情况。
函数原型如下:
c
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
参数解释:
fd:文件描述符,即要读取的文件或设备的标识符。
iov:一个iovec结构的数组,定义了要读取的数据块的大小和位置。
iovcnt:数组中iovec结构的数量。
readv函数通过iovec结构来指定读取的数据块。每个iovec结构包含两个成员:iov_base和iov_len。iov_base指向要存储读取数据块的位置,iov_len指定了要读取的字节数。函数会尝试读取所有指定的数据块,并把读取的总字节数返回。如果出现错误,会返回-1并设置全局变量errno。
这个函数可以减少系统调用的次数,提高程序的效率。例如,如果你需要从文件中读取1000个不同大小的数据块,使用普通的read函数,可能需要执行1000次系统调用。而使用readv函数,只需要一次系统调用就可以完成所有的读取操作。
需要注意的是,虽然readv可以提高效率,但由于需要处理更多的数据块,因此在编程时需要更仔细地处理边界条件和错误情况。例如,你需要确保有足够的空间来存储所有读取的数据块,同时也要处理可能的错误情况并正确地处理它们。
writev()
writev函数是Linux系统中的一个I/O操作函数,它允许用户程序在一次系统调用中向文件描述符写入多个分散的数据块。这个函数特别适合于需要向文件或其他I/O设备写入不同大小的数据块的情况。
函数原型如下:
c
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
参数解释:
fd:文件描述符,即要写入的文件或设备的标识符。
iov:一个iovec结构的数组,定义了要写入的数据块的大小和位置。
iovcnt:数组中iovec结构的数量。
writev函数通过iovec结构来指定要写入的数据块。每个iovec结构包含两个成员:iov_base和iov_len。iov_base指向要读取的数据块的位置,iov_len指定了要写入的字节数。函数会尝试写入所有指定的数据块,并把写入的字节数返回。如果出现错误,会返回-1并设置全局变量errno。
与readv函数类似,writev函数也可以减少系统调用的次数,提高程序的效率。例如,如果你需要向文件中写入1000个不同大小的数据块,使用普通的write函数,可能需要执行1000次系统调用。而使用writev函数,只需要一次系统调用就可以完成所有的写入操作。
需要注意的是,虽然writev可以提高效率,但由于需要处理更多的数据块,因此在编程时需要更仔细地处理边界条件和错误情况。例如,你需要确保有足够的数据块来填充所有的写入请求,同时也要处理可能的错误情况并正确地处理它们。
存储映射IO
内存映射I/O(Memory-mapped I/O)是一种在Linux和其他类Unix操作系统中访问I/O设备的技术。它将一个设备或文件的一部分映射到进程的地址空间中,从而使进程可以像访问内存一样访问设备或文件。
具体来说,内存映射I/O允许应用程序通过指针直接访问设备内存,而无需通过传统的I/O系统调用(如read和write)。这大大减少了CPU在用户和内核模式之间的上下文切换次数,从而提高了性能。
内存映射I/O的实现依赖于虚拟内存系统。当一个设备或文件被映射到进程的地址空间时,操作系统会为该设备或文件分配一段虚拟内存。应用程序可以通过访问这段虚拟内存来读写设备或文件。在底层,操作系统会处理所有的内存管理细节,包括页表的更新和缓存一致性的维护。
内存映射I/O在很多场合中都很有用。例如,在网络编程中,它允许应用程序直接访问网络设备的缓冲区,从而提高网络传输的效率。在文件系统中,它允许应用程序直接访问文件的数据块,从而避免了传统的文件I/O调用带来的开销。此外,一些硬件设备也支持内存映射I/O,这使得应用程序可以直接访问设备的寄存器和内存,从而提高了设备的性能和可编程性。
需要注意的是,虽然内存映射I/O可以提高性能,但它也有一些潜在的陷阱。例如,如果多个进程同时映射同一个设备或文件,它们之间可能会产生竞态条件。此外,如果应用程序错误地访问了不属于它的内存地址,可能会导致不可预测的行为或系统崩溃。因此,在使用内存映射I/O时,需要仔细考虑其安全性和正确性。
mmap()
mmap函数是在Linux系统中的一个系统调用,它用于将一个文件或者其他对象映射进内存。它提供了一种灵活且高效的方式来访问和操作文件数据,无需通过传统的read和write等I/O操作。
函数原型如下:
c
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
参数解释:
start:指定映射区域的起始地址,通常设为NULL,由系统决定映射区域的起始地址。
length:指定映射区域的长度。
prot:指定映射区域的保护方式。例如,只读(PROT_READ)、只写(PROT_WRITE)或读写(PROT_READ | PROT_WRITE)。
flags:指定映射区域的类型。例如,一般映射(MAP_SHARED)、私有的只写映射(MAP_PRIVATE)等。
fd:指定要映射的文件描述符。
offset:指定文件中开始映射的偏移量。
mmap函数将文件映射到进程的地址空间后,你可以通过指针访问该内存区域,就像访问普通内存一样。这种方式的优点是效率高,因为内存访问速度远快于磁盘I/O。同时,通过内存映射,多个进程可以共享同一文件的数据,实现进程间的数据共享。
需要注意的是,使用mmap函数时需要小心处理内存管理问题,确保不会出现内存越界或者竞争条件等问题。同时,mmap函数的使用也需要考虑文件的大小和物理内存的大小等因素,确保操作的合理性和可行性。
munmap()
munmap函数是在Linux系统中的一个系统调用,用于撤销之前通过mmap函数进行的内存映射。
函数原型如下:
c
#include <sys/mman.h>
int munmap(void *addr, size_t length);
参数解释:
addr:指定要撤销映射的内存区域的起始地址,这个地址是由之前的mmap函数返回的。
length:指定要撤销映射的内存区域的长度。
当进程不再需要访问通过mmap映射的内存区域时,或者需要释放该内存区域以便于操作系统可以再次将其分配给其他进程时,就需要调用munmap函数。调用munmap后,之前通过mmap映射进内存的区域将不再有效,不可以再被访问。
需要注意的是,munmap函数只撤销映射,并不会释放映射的内存区域。实际上,这部分内存区域仍然属于进程,直到进程结束。因此,在调用munmap后,进程不应再试图访问已经撤销映射的内存区域,否则可能会导致错误或崩溃。
总的来说,munmap函数是Linux系统中处理内存映射的一个重要工具,可以帮助我们有效地管理和控制内存的使用。
实例
文件锁
Linux中的文件锁(File Lock)是一种机制,用于在多用户环境中保护文件的共享使用,避免产生竞争状态。文件锁包括建议性锁和强制性锁。
建议性锁是一种非强制性的锁,只对参与规则的协作进程有效,其他进程则可以随意更改文件。建议性锁要求每个使用上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。在一般情况下,内核和系统都不使用建议性锁,它们依靠程序员遵守这个规定。
强制性锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他任何文件对其进行读写操作。采用强制性锁对性能的影响很大,每次读写操作都必须检查是否有锁存在。在Linux中,实现文件上锁的函数有lockf()和fcntl(),其中lockf()用于对文件施加建议性锁,fcntl()不仅可以施加建议性锁,还可以施加强制锁。
对于强制性锁,可以进一步细分为共享锁和互斥锁。共享锁也称为读锁,可以多个进程进行共享锁,只能对文件进行读操作。互斥锁也称为写锁,一次只有一个进程能进行互斥锁,可以对文件进行读和写操作。
总的来说,文件锁是Linux系统中用于多用户环境中保护文件共享使用的一种机制,可以避免竞争状态,同时需要注意不同类型锁的特性和使用方式。
很多函数都可以实现文件锁的功能,这里只介绍下面两种。
fcntl()
该函数之前提过,不再赘述。
lockf()
lockf函数是Linux系统中的一个函数,用于对文件施加建议性锁。它的原型如下:
c
#include <unistd.h>
int lockf(int fd, int cmd, off_t len);
参数解释:
fd:指定要上锁的文件的文件描述符。
cmd:指定要进行的操作,可以是以下值之一:F_LOCK:对文件进行建议性上锁。
F_TLOCK:与F_LOCK类似,但是如果无法立即获取锁,则调用将失败并返回错误,而不是使调用进程进入睡眠状态。
F_ULOCK:解锁文件。
F_TEST:测试文件是否被上锁。
len:指定要上锁的字节数。将要上锁的字节数设为0表示对整个文件进行上锁。也就是说,调用lockf(fd, F_LOCK, 0)会对文件描述符为fd的文件进行上锁,锁的范围覆盖整个文件。这种操作通常用于对整个文件进行保护,避免其他进程对其进行读写操作。
lockf函数可以用于对文件进行建议性锁,这种锁是可协商的,不会强制阻止其他进程对文件进行操作。它只提供了一种协作机制,要求参与规则的进程都遵守上锁的规定。当一个进程使用lockf函数对文件进行上锁时,其他进程也可以通过调用同样的函数来查看或协商锁的状态。
如果要对文件施加强制性锁,可以使用fcntl函数。与lockf不同的是,fcntl可以施加强制性锁,这种锁会阻止其他进程对文件进行操作,直到该锁被释放。
需要注意的是,使用文件锁需要谨慎处理,不当的使用可能会导致死锁或其他问题。因此,在使用文件锁时应该遵循一些最佳实践,例如:尽量短时间持有锁、避免嵌套锁等。