linux中fd的几点理解——一切皆文件

本文深入探讨Linux中的文件描述符(FD)概念,包括其进程级特性、一切皆文件的理念、fdtable结构及fd在进程间的传递方式。此外,还介绍了并发写文件时如何避免数据错乱和保证原子性的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值