fd 全称是 file descriptor,文件描述符,又称句柄。当我们在打开一个文件,创建一个 socket, 创建一个 epoll 时,返回值往往都用一个变量 int fd 来表示。
1 fd 是进程级别的资源,不是系统级的资源
(1)fd 是进程级别的资源,一个文件可以对应用户态的很多 fd
当我们打开一个文件时,比如 /home/test 文件,会返回一个 fd。这个 fd 和 /home/test 并不是唯一对应的。
如下图所示,一个文件可以在一个应用内部打开多次,两次返回的 fd 肯定是不一样的;一个文件也可以被不同的应用打开,两个应用内的 fd 可能相同,也可能不相同。也就是说,一个文件可以对应用户态的很多 fd。
如下代码所示,同一个文件可以在进程内打开多次,并且返回的 fd 是不一样的。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int open_file() {
int fd;
char filename[] = "/home/test";
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open file failed");
return -1;
}
char buffer[16] = {'\0'};
int n = read(fd, buffer, 16);
if (n > 0) {
printf("read content: %s", buffer);
} else if (n == -1) {
perror("read file error");
} else {
printf("EOF\n");
}
return fd;
}
int main() {
int fd1 = open_file();
int fd2 = open_file();
printf("fd1: %d, fd2: %d\n", fd1, fd2);
sleep(30);
close(fd1);
close(fd2);
return 0;
}
(2)内核的 struct inode 是系统资源,一个文件只有一个
既然用户态的 fd 和文件不是唯一对应的,那么在系统中,什么对象和一个文件是唯一对应的呢?
inode
在 linux 中,一个文件只有一个 inode,无论在用户态有多少个应用打开这个文件,无论打开多少次,在内核中只有一个 struct inode 用来表示一个文件。struct inode 包括的内容比较多,其中比较好理解的是 inode 号(每个文件有一个唯一的 id),文件创建的时间,最近一次访问的时间,最近一次修改的时间。
使用 stat 命令也可以查看一个文件的上述信息,如下图所示,stat 命令中显示了文件的 inode 号,以及最近访问时间,最近改动时间和创建时间。
(3)一个应用可以打开的文件个数有限制
一个应用可以打开的 fd 的个数是有限制的,使用 ulimit -a 可以看到,其中 open files 这一项就是限制一个应用能打开的 fd 的个数,我的系统中是 1024。ulimit -a 显示的是系统的很多资源限制,如果想单独查看打开的文件的个数的限制,可以使用 ulimit -n 进行查看。也可以在 ulimit -n 后边加一个参数,对这个限制进行修改,比如 ulimit -n 2000 设置之后,一个应用最多就可以打开 2000 个文件。
为了验证 fd 个数限制,我们是用下边的代码进行测试,代码中尝试打开 2000 次文件,并且打开失败时退出循环。
结果截图如下,可以看到打开 1021 次之后,就失败了,失败信息是 “Too many open files”。之所以打开了 1021 个,而不是 1024 个,这是因为每个应用默认就会使用掉 3 个 fd,0、1、2,分别是标准输入,标准输出,标准错误。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int open_file() {
int fd;
char filename[] = "/home/test";
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open file failed");
return -1;
}
return fd;
}
int main() {
for (int i = 0; i < 2000; i ++) {
int fd = open_file();
printf("i: %d, fd: %d\n", i, fd);
if (fd == -1) {
break;
}
}
sleep(30);
return 0;
}
2 linux 一切皆文件的理解
一切皆文件,本人理解说的是,站在用户态使用系统资源的时候,都可以通过打开一个 fd,然后自己想要的操作都可以通过读写 fd 来完成。操作磁盘上的文件,本身就是文件;通信中使用的 socket,多路复用技术中的 epoll,也都是创建了一个 fd;进程间通信中的管道,共享内存,创建的时候也是返回一个 fd。
传统意义上的文件是指磁盘中的文件,比如一个文本文件,一个照片,一个可执行文件,一个动态库,这些都是存储在磁盘中的文件。一切皆文件,并不是说 socket,epoll 都是磁盘上的文件,而是使用文件的思想来完成对应的功能。以 socket 为例,创建一个 socket 也是返回一个 fd,可以对这个 fd 进行 read(), write() 来收发数据。
内核基础:
(1)struct file_operations
struct file_operations 定义在文件 include/linux/fs.h。结构体的详细定义如下图所示,这个结构体中定义了一些接口,不同的文件系统,比如磁盘文件,socket, epoll 需要实现自己的接口。这也体现了面向对象的思想,接口抽象,实现,多态。
read() | 读 |
write() | 写 |
poll() | 获取内容 |
open() | 打开 |
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
bool may_pollfree;
} __randomize_layout;
下面以 socket 为例,介绍 socket 对 struct file_operations 的实现。socket 实现的结构体是 socket_file_ops,我们重点关注 read_iter 和 write_iter,通过这两个函数可以收发数据。
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
.show_fdinfo = sock_show_fdinfo,
};
以接收数据为例,子用户态通过调用 read() 来接收数据。
// read() 系统调用定义如下,read() 中调用 ksys_read()
// fs/read_write.c
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
// ksys_read() 中调用了 vfs_read()
// vfs 相当于把不同的文件系统的接口统一起来,在 vfs 层调用不同文件系统的实现
// 类似于面向对象中,创建一个子类对象可以赋值给一个声明为父类的变量
// 然后通过这个父类变量调用方法又可以调用到子类的方法
// fs/read_write.c
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
...
ret = vfs_read(f.file, buf, count, ppos);
...
}
// vfs_read() 中首先判断有没有实现 read() 方法,如果实现了则调用 read()
// 如果没有实现 read() 方法则判断有没有实现 read_iter 方法,如果实现了则调用 new_sync_read()
// socket_file_ops 中实现了 read_iter() 方法,没有实现 read() 方法
// fs/read_write.c
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
...
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
ret = new_sync_read(file, buf, count, pos);
...
return ret;
}
最终调用到了 sock_read_iter() 来接收数据,sock_read_iter() 中调用了 sock_recvmsg()。
对于 tcp 来说,也可以通过 recv() 和 send() 来收发数据。通过 recv() 来接收数据,没有经过 vfs 层,最终也是调用到 sock_recvmsg() 来接收数据。
如下是通过 read(), write() 收发数据的例子。
server:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <unistd.h>
#define MAXLINE 1024
int main(int argc, char *argv[])
{
// 1. 创建一个监听 socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0)
{
fprintf(stderr, "socket error : %s\n", strerror(errno));
return -1;
}
// 2. 初始化服务器地址和端口
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
// 3. 绑定地址+端口
if(bind(listenfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) < 0)
{
fprintf(stderr,"bind error:%s\n", strerror(errno));
return -1;
}
printf("begin listen....\n");
// 4. 开始监听
if(listen(listenfd, 128))
{
fprintf(stderr, "listen error:%s\n\a", strerror(errno));
exit(1);
}
// 5. 获取已连接的socket
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr);
int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_addrlen);
if(clientfd < 0) {
fprintf(stderr, "accept error:%s\n\a", strerror(errno));
exit(1);
}
printf("accept success\n");
char message[MAXLINE] = {0};
while(1) {
//6. 读取客户端发送的数据
int n = read(clientfd, message, MAXLINE);
if(n < 0) { // 读取错误
fprintf(stderr, "read error:%s\n\a", strerror(errno));
break;
} else if(n == 0) { // 返回 0 ,代表读到 FIN 报文
fprintf(stderr, "client closed \n");
sleep(1);
close(clientfd); // 没有数据要发送,立马关闭连接
break;
}
message[n] = 0;
printf("received %d bytes: %s\n", n, message);
}
close(listenfd);
return 0;
}
client:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main(int argc, char *argv[])
{
// 1. 创建一个监听 socket
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
if(connectfd < 0)
{
fprintf(stderr, "socket error : %s\n", strerror(errno));
return -1;
}
// 2. 初始化服务器地址和端口
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8888);
// 3. 连接服务器
if(connect(connectfd, (struct sockaddr *)(&server_addr), sizeof(server_addr)) < 0)
{
fprintf(stderr,"connect error:%s\n", strerror(errno));
return -1;
}
printf("connect success\n");
char sendline[64] = "hello, i am xiaolin";
//4. 发送数据
// int ret = send(connectfd, sendline, strlen(sendline), 0);
int ret = write(connectfd, sendline, strlen(sendline));
if(ret != strlen(sendline)) {
fprintf(stderr,"send data error:%s\n", strerror(errno));
return -1;
}
printf("already send %d bytes\n", ret);
sleep(1);
//5. 关闭连接
close(connectfd);
return 0;
}
3 fd table
fd 代表着打开的文件,是进程级别的资源。在 linux 内核中,使用 struct task_struct 来表示一个进程,在 struct task_struct 中有一个成员是 struct files_struct *files,这个成员管理着应用打开的文件。
struct task_struct {
...
/* Open file information: */
struct files_struct *files;
...
}
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
最终,fd 是保存在了 struct fdtable 中的 struct file __rcu **fd,是一个数组,如下图所示,数组的下标是 fd,元素是一个 struct file *。所以,fd 和 struct file 都可以表示一个打开的文件,属于进程级资源。
struct file 中有一个成员为 const struct file_operations *f_op,该成员中记录着 fd 的操作集,比如如果是通过 socket 创建的 fd,那么对应的就是 socket_file_ops 中的操作集。当系统调用 read(), write() 操作 fd 时,内核首先会通过 fd 在 fd table 中找到对应的 struct file,找到 struct file 就找到了操作集,然后句可以进行操作了。
4 fd 在进程间的传递
fd 表示打开的文件,是进程级的资源,一个进程可以重复打开同一个文件,每次的 fd 是不同的,不同的进程也可以打开相同的文件,fd 可能相同,也可能不同。
如果一个进程 A 想访问另一个进程 B 打开的文件,如果进程 B 中的 fd 是 10,那么是不是进程 B 把 10 直接传递给 A,A 就可以通过 read() 或者 write() 读写这个文件了呢。这样是无法工作的。fd 10 在 A 中仅仅表示一个整数而已,并不代表一个打开的文件。想想也能理解,进程 B 在打开一个文件,返回 fd 10 的时候,不仅仅是用户态拿到了一个文件描述符 10,同时在内核也维护着很多的内容,进程 A 只使用一个整数 10,并没有在内核维护对应的内容,这样通过 read(), write() 把 10 传递给内核的时候,内核并不认识这个 fd。
fd 在进程之间也是可以传递的,通过 unix socket 和 cmsg 来实现。进程 B 将 fd 10 传递给进程 A 之后,在 A 中并不一定也是 10,也可能是其它数,但是它们都指向同一个文件。
unix socket 和 cmsg 中的 SCM_RIGHTS 专门用来在进程之间传递 fd。
下边是传递 fd 的代码,server 端代码和 client 端代码。编译之后,先运行 server 再运行 client,可以看到 server 写的内容,可以在 client 端读取到。
server 端,发送 fd:
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#define SOCKET_PATH "./unix_socket_fd_transmit"
int create_unix_server_and_accept_connection() {
int server_fd;
int connection_fd;
int result;
struct sockaddr_un sun;
server_fd = socket(PF_UNIX, SOCK_STREAM, 0);
printf("server fd: %d\n", server_fd);
unlink(SOCKET_PATH);
memset(&sun, 0, sizeof(sun));
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path, SOCKET_PATH);
int len = offsetof(struct sockaddr_un, sun_path) + strlen(sun.sun_path) + 1;
if (bind(server_fd, (struct sockaddr *)&sun, len) != 0) {
printf("bind error\n");
return -1;
}
listen(server_fd, 8);
struct sockaddr_un client_un;
int un_len = sizeof(client_un);
connection_fd = accept(server_fd, (struct sockaddr *)&client_un, &un_len);
return connection_fd;
}
void send_fd(int socket_fd, int fd_to_be_send) {
struct msghdr msg;
struct cmsghdr *cmsg = NULL;
int io[3] = {0, 1, 2};
char buf[CMSG_SPACE(sizeof(io))];
struct iovec iov;
int dummy;
memset(&msg, 0, sizeof(struct msghdr));
iov.iov_base = &dummy;
iov.iov_len = 1;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_len = CMSG_LEN(sizeof(fd_to_be_send));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
memcpy(CMSG_DATA(cmsg), &fd_to_be_send, sizeof(fd_to_be_send));
msg.msg_controllen = cmsg->cmsg_len;
int size = sendmsg(socket_fd, &msg, 0);
return;
}
int main(int argc, char const *argv[]) {
int connection_fd = create_unix_server_and_accept_connection();
int fd_to_be_send = open("./test", O_RDWR | O_CREAT);
char buf[] = "test transmit fd by unix socket between process";
write(fd_to_be_send, buf, sizeof(buf) + 1);
printf("sender seek cur: %ld, ", lseek(fd_to_be_send, 0, SEEK_CUR));
printf("connection fd: %d, fd to be send: %d\n", connection_fd, fd_to_be_send);
send_fd(connection_fd, fd_to_be_send);
sleep(100);
return 0;
}
client 端,接收 fd:
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define SOCKET_PATH "./unix_socket_fd_transmit"
int create_unix_connection() {
int fd;
int result;
struct sockaddr_un sun;
fd = socket(PF_UNIX, SOCK_STREAM, 0);
sun.sun_family = AF_UNIX;
strncpy(sun.sun_path, SOCKET_PATH, sizeof(sun.sun_path) - 1);
connect(fd, (struct sockaddr *)&sun, sizeof(sun));
return fd;
}
void receive_fd(int socket_fd) {
int dummy = 0;
int received_fd;
struct iovec iov;
iov.iov_base = &dummy;
iov.iov_len = 1;
char buf[CMSG_SPACE(sizeof(received_fd))];
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
struct cmsghdr *cmsg;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_len = CMSG_LEN(sizeof(received_fd));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
memcpy(CMSG_DATA(cmsg), &received_fd, sizeof(received_fd));
int size = recvmsg(socket_fd, &msg, 0);
memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(received_fd));
printf("received fd: %d\n", received_fd);
printf("receiver seek cur: %ld\n", lseek(received_fd, 0, SEEK_CUR));
lseek(received_fd, 0, SEEK_SET);
char receive_buf[128] = {'\0'};
int read_rc = read(received_fd, receive_buf, 128);
printf("content in received fd: %s, read_rc: %d\n", receive_buf, read_rc);
}
int main(int argc, char const *argv[]) {
int socket_fd;
int fds[3];
socket_fd = create_unix_connection();
receive_fd(socket_fd);
return 0;
}
5 对一个文件进行并发写
测试方案 | 内容会不会错乱 | 如何保证原子性 |
多个线程写一个 fd | 不会 | |
多线程写多个 fd,多个 fd 指向同一个文件 | 会 | 加锁 |
多进程写多个 fd,多个 fd 指向同一个文件 | 会 | 文件锁 |
(1)多线程写同一个 fd
如下代码,创建了两个线程,文件 test 打开一次,两个线程同时往 fd 中写数据。程序执行完毕之后,查看 test 中的内容,没有发现数据错乱的情况。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int fd;
void* do_write(void* arg) {
char* data = (char*)arg;
for (int i = 0; i < 100; i++) {
write(fd, data, strlen(data));
}
}
int main() {
fd = open("test", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}
pthread_t thread1;
pthread_t thread2;
char* data = "aaaa aaaa aaaa aaaa aaaa.\n";
char* data1 = "bbbb bbbb bbbb bbbb bbbb.\n";
pthread_create(&thread1, NULL, do_write, (void*)data);
pthread_create(&thread2, NULL, do_write, (void*)data1);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
close(fd);
return 0;
}
(2)多个线程写多个 fd
如下代码,test 文件打开两次,返回 fd1 和 fd2,两个线程分别向 fd1 和 fd2 中写数据。在不加锁的情况下,会出现内容的覆盖。两个线程共写了 200 行数据,文件中实际保存的数据少于 200 行。
如果把 do_write() 中的锁打开,那么就不会发生覆盖。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int fd1;
int fd2;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* do_write(void* arg) {
char* data = (char*)arg;
int fd = 0;
if (data[0] == 'a') {
fd = fd1;
} else {
fd = fd2;
}
printf("fd %d\n", fd);
for (int i = 0; i < 100; i++) {
// pthread_mutex_lock(&mutex);
lseek(fd, 0, SEEK_END);
write(fd, data, strlen(data));
// pthread_mutex_unlock(&mutex);
}
}
int main() {
fd1 = open("test", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1) {
perror("open");
return 1;
}
fd2 = open("test", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd2 == -1) {
perror("open");
return 1;
}
pthread_t thread1, thread2;
char* data1 = "aaaa aaaa aaaa aaaa aaaa.\n";
char* data2 = "bbbb bbbb bbbb bbbb.\n";
pthread_create(&thread1, NULL, do_write, (void*)data1);
pthread_create(&thread2, NULL, do_write, (void*)data2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
close(fd1);
close(fd2);
return 0;
}
(3)多进程同时写一个文件
一个进程内的多个线程同时写一个文件,就会出现数据的覆盖和错乱,多进程同时写一个文件,当然也会出现数据的覆盖和错乱。
进程内的多线程之间可以使用自旋锁或者 mutex 进行同步,多进程之间对一个文件进行操作时,可以通过文件锁来实现同步。
文件锁用一个结构体来实现,struct flock,各成员如下:
struct flock {
short l_type; /* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, or SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
};
l_whence, l_start, l_len 3 个成员共同决定了文件锁定的范围。
l_whence 取值有 3 个,分别是 SEEK_SET,SEEK_CUR, SEEK_END,分别表示文件的开头,当前位置,文件的结尾。
l_start 相对于 l_whence 的偏移
l_len 保护的长度
下边代码中 l_whence 取值 SEEK_SET,l_start 取值 0, l_len 取值 0,表示对文件的所有内容进行保护。如何验证 flock 是否生效呢,我们可以改动一下下边的代码,获取到锁之后不释放锁,然后起动两个进程,可以看到其中一个进程获取到锁之后可以一直向文件写数据,但是另外一个进程一直在等锁。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
const char* filename = "test";
int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd == -1) {
perror("Error opening file");
exit(EXIT_FAILURE);
}
struct flock fl;
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
for (int i = 0; i < 1000; i++) {
printf("1111 before get lock\n");
if (fcntl(fd, F_SETLKW, &fl) == -1) {
perror("Error locking file");
close(fd);
exit(EXIT_FAILURE);
}
printf("1111 after get lock\n");
const char* message = "aaaa aaaa aaaa aaaa\n";
if (write(fd, message, strlen(message)) == -1) {
perror("Error writing to file");
}
sleep(1);
printf("1111 before unlock\n");
fl.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &fl) == -1) {
perror("Error unlocking file");
}
printf("1111 after unlock\n");
}
close(fd);
return 0;
}