当前位置: 首页 > news >正文

Linux文件数据写入

结构体

fd

fd也就是文件描述符,用于标识已经打开的文件、管道、socket等。是进程和内核的桥梁,允许进程执行各种文件操作

struct fd {struct file *file;unsigned int flags;
};
file

Linux内核中表示打开文件的结构体,包含了文件操作所需的各种信息和元数据。这是文件系统操作的核心结构之一,允许内核跟踪每个打开的文件及其相关的状态。

struct file {// 用于链接或者引用计数union {// 链表节点struct llist_node	fu_llist;// Read-Copy-Update头struct rcu_head 	fu_rcuhead;} f_u;// 文件路径信息struct path		f_path;// 文件的 inode 结构体,表示文件的具体内容和属性struct inode		*f_inode;	/* cached value */// 指向文件操作结构体的指针,包含与文件相关的各种操作函数指针,如读、写、打开、关闭等const struct file_operations	*f_op;/** Protects f_ep_links, f_flags.* Must not be taken from IRQ context.*/spinlock_t		f_lock;enum rw_hint		f_write_hint;// 引用计数,表示有多少引用指向这个文件结构体atomic_long_t		f_count;// 文件标志,描述文件的各种属性,如只读、只写、非阻塞等unsigned int 		f_flags;// 文件模式,指示文件的打开模式,如读、写、执行等fmode_t			f_mode;// 位置锁,用于保护文件读写位置的锁struct mutex		f_pos_lock;// 文件的读写位置偏移量loff_t			f_pos;// 文件所有者结构体,包含文件的拥有者和访问权限信息struct fown_struct	f_owner;const struct cred	*f_cred;// 文件预读取状态结构体,包含文件预读取的相关信息struct file_ra_state	f_ra;// 文件版本号,表示文件的版本信息。u64			f_version;
#ifdef CONFIG_SECURITYvoid			*f_security;
#endif/* needed for tty driver, and maybe others */void			*private_data;#ifdef CONFIG_EPOLL/* Used by fs/eventpoll.c to link all the hooks to this file */// 用于事件轮询(epoll)系统调用的链表结struct list_head	f_ep_links;struct list_head	f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */// 地址空间指针,表示文件的内存映射状态struct address_space	*f_mapping;// 写回错误序列号,用于跟踪文件写回操作的错误errseq_t		f_wb_err;
} __randomize_layout__attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */
inode

inode包含文件的所有元数据,支撑访问控制、文件操作、同步、状态管理和特定文件类型支持

/** Keep mostly read-only and often accessed (especially for* the RCU path lookup and 'stat' data) fields at the beginning* of the 'struct inode'*/
struct inode {// 文件的模式,包括文件类型和文件权限umode_t			i_mode;// 操作标志,标识文件系统特定的操作unsigned short		i_opflags;// 文件所有者的用户 IDkuid_t			i_uid;// 文件所有者的组 IDkgid_t			i_gid;// 文件标志unsigned int		i_flags;#ifdef CONFIG_FS_POSIX_ACLstruct posix_acl	*i_acl;struct posix_acl	*i_default_acl;
#endif// 指向 inode 操作函数的指针const struct inode_operations	*i_op;// 指向文件系统超级块struct super_block	*i_sb;// 地址空间,描述文件内容在内存中的映射struct address_space	*i_mapping;#ifdef CONFIG_SECURITYvoid			*i_security;
#endif/* Stat data, not accessed from path walking */// inode 号,唯一标识一个文件unsigned long		i_ino;/** Filesystems may only read i_nlink directly.  They shall use the* following functions for modification:**    (set|clear|inc|drop)_nlink*    inode_(inc|dec)_link_count*/// 链接数,表示有多少个目录项指向此 inodeunion {const unsigned int i_nlink;unsigned int __i_nlink;};// 设备号,对于设备文件有效dev_t			i_rdev;// 文件大小loff_t			i_size;struct timespec64	i_atime;struct timespec64	i_mtime;struct timespec64	i_ctime;spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */// 文件字节数、块大小位数、写入提示、文件占用块数unsigned short          i_bytes;u8			i_blkbits;u8			i_write_hint;blkcnt_t		i_blocks;#ifdef __NEED_I_SIZE_ORDEREDseqcount_t		i_size_seqcount;
#endif/* Misc */// 文件状态unsigned long		i_state;// 读写信号量,用于同步struct rw_semaphore	i_rwsem;unsigned long		dirtied_when;	/* jiffies of first dirtying */unsigned long		dirtied_time_when;// 特定结构:hash链表节点、IO列表struct hlist_node	i_hash;struct list_head	i_io_list;	/* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACKstruct bdi_writeback	*i_wb;		/* the associated cgroup wb *//* foreign inode detection, see wbc_detach_inode() */int			i_wb_frn_winner;u16			i_wb_frn_avg_time;u16			i_wb_frn_history;
#endif// 用于缓存回收的LRU列表struct list_head	i_lru;		/* inode LRU list */// 用于管理同一超级块中的 inode的超级块链表struct list_head	i_sb_list;// 用于写回缓冲的写回列表struct list_head	i_wb_list;	/* backing dev writeback list */union {struct hlist_head	i_dentry;struct rcu_head		i_rcu;};// inode版本号atomic64_t		i_version;// 引用计数atomic_t		i_count;// 直接IO计数atomic_t		i_dio_count;// 写操作计数atomic_t		i_writecount;
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)atomic_t		i_readcount; /* struct files open RO */
#endifunion {const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */void (*free_inode)(struct inode *);};struct file_lock_context	*i_flctx;struct address_space	i_data;struct list_head	i_devices;union {struct pipe_inode_info	*i_pipe;struct block_device	*i_bdev;struct cdev		*i_cdev;char			*i_link;unsigned		i_dir_seq;};__u32			i_generation;#ifdef CONFIG_FSNOTIFY__u32			i_fsnotify_mask; /* all events this inode cares about */struct fsnotify_mark_connector __rcu	*i_fsnotify_marks;
#endif#ifdef CONFIG_FS_ENCRYPTIONstruct fscrypt_info	*i_crypt_info;
#endif#ifdef CONFIG_FS_VERITYstruct fsverity_info	*i_verity_info;
#endifvoid			*i_private; /* fs or device private pointer */
} __randomize_layout;

写入——从write()到vfs

write()系统调用在内核的实现为sys_write。

本部分在真正文件系统操作调用之外,只是获取释放文件描述符、更新位置指针、写入前检查等操作

ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{// 获取文件描述符fdstruct fd f = fdget_pos(fd);ssize_t ret = -EBADF;if (f.file) {// 获取文件当前位置指针loff_t pos, *ppos = file_ppos(f.file);if (ppos) {pos = *ppos;ppos = &pos;}// VFS执行实际写操作ret = vfs_write(f.file, buf, count, ppos);// 更新文件指针位置if (ret >= 0 && ppos)f.file->f_pos = pos;// 释放文件描述符,减少其引用计数fdput_pos(f);}return ret;
}

接着进入vfs,vfs实际也是调用真正文件系统的接口实现

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{ssize_t ret;// 检查文件是否可写if (!(file->f_mode & FMODE_WRITE))return -EBADF; // 文件不可写,返回错误码 EBADFif (!(file->f_mode & FMODE_CAN_WRITE))return -EINVAL; // 文件模式不支持写操作,返回错误码 EINVAL// 检查用户空间指针是否有效if (unlikely(!access_ok(buf, count)))return -EFAULT; // 用户空间指针无效,返回错误码 EFAULT// 验证写操作范围ret = rw_verify_area(WRITE, file, pos, count);if (!ret) {// 限制最大写入字节数if (count > MAX_RW_COUNT)count = MAX_RW_COUNT;// 开始文件写入file_start_write(file);// 实际执行写操作ret = __vfs_write(file, buf, count, pos);// 如果写入成功,发送文件系统通知并更新写字节数if (ret > 0) {fsnotify_modify(file);add_wchar(current, ret);}// 更新系统调用写计数inc_syscw(current);// 结束文件写入file_end_write(file);}return ret;
}
static ssize_t __vfs_write(struct file *file, const char __user *p,size_t count, loff_t *pos)
{// 首先检查文件操作结构是否有write方法,有直接用if (file->f_op->write)return file->f_op->write(file, p, count, pos);else if (file->f_op->write_iter)return new_sync_write(file, p, count, pos);elsereturn -EINVAL;
}

以下是ext4文件系统实现vfs接口的方法

const struct file_operations ext4_file_operations = {.llseek		= ext4_llseek,.read_iter	= ext4_file_read_iter,.write_iter	= ext4_file_write_iter,.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT.compat_ioctl	= ext4_compat_ioctl,
#endif.mmap		= ext4_file_mmap,.mmap_supported_flags = MAP_SYNC,.open		= ext4_file_open,.release	= ext4_release_file,.fsync		= ext4_sync_file,.get_unmapped_area = thp_get_unmapped_area,.splice_read	= generic_file_splice_read,.splice_write	= iter_file_splice_write,.fallocate	= ext4_fallocate,
};

ext4 buffered or direct

在Linux中存在几种不同的IO写入方式

  • DAX: 字节级别的操作。要求额外的硬件支持

  • DIO:直接从用户态写入数据到硬盘中,跳过内核缓冲区,减少了上下文切换和数据复制开销

    块级别操作,数据的读写需要是设备的块大小和linux系统的页大小的整数倍

  • BIO:默认标准方式。数据会先从应用程序的地址空间拷贝到 操作系统内核地址空间的页缓存,然后再写入磁盘。根据Linux的延迟写机制,当数据写到操作系统内核地址空间的页缓存就意味write

    缓冲写入操作通常是异步的,数据首先写入页缓存,后续由内核的pdflush守护进程或kworker线程将缓存数据写入磁盘。直接I/O则是同步的,数据直接写入磁盘。

static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{// 获取文件关联的inodestruct inode *inode = file_inode(iocb->ki_filp);if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))return -EIO;// 如果文件系统配置支持直接访问,且inode也允许,则进行直接写入
#ifdef CONFIG_FS_DAXif (IS_DAX(inode))return ext4_dax_write_iter(iocb, from);
#endif// 如果IO控制块设置了IOCB_DIRECT,则执行直接IO写入,绕过页缓存if (iocb->ki_flags & IOCB_DIRECT)return ext4_dio_write_iter(iocb, from);// 否则进行缓存写入return ext4_buffered_write_iter(iocb, from);
}
extent

在以下代码中出现了extent,那么extent是什么呢?

extent是一段连续的物理块,表示文件数据在磁盘上的位置和长度。

  • 起始块
  • 物理块
  • 长度

每个文件都有一个与之关联的 extent 树,其根节点存储在 inode 中。树中的节点包含 extent 或指向子节点的指针。

  • 叶子节点:存储实际的 extent 信息(起始块、物理块和长度)

  • 内部节点:存储指向下一级节点的指针。

内联数据

内联数据适用于包含大量小文件场景,将小文件数据直接储存到文件系统的元数据结构中,可以减少空间浪费

孤儿列表

孤儿列表用于跟踪在文件操作中可能会被中途删除或者截断的文件,确保即使在系统崩溃的情况下也能被正确处理

比如,在文件删除中,inode被更新表示文件被删除了,但是系统中途崩溃了,而实际删除工作在后面进行,就会导致这些文件变为孤儿,文件元数据仍然存在,可是文件本身被逻辑删除了

ext4 buffered IO

buffered IO部分主要做了以下事情

  • 锁定inode,防止并发修改,保证page缓存的一致性
  • 检查写入操作是否合法,并进行一些预处理
  • 写入
static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,struct iov_iter *from)
{ssize_t ret;struct inode *inode = file_inode(iocb->ki_filp);// 如果 iocb 的标志中包含 IOCB_NOWAIT,则返回不支持的操作错误if (iocb->ki_flags & IOCB_NOWAIT)return -EOPNOTSUPP;// 加锁以保护 inode 数据结构inode_lock(inode);// 检查写入操作是否合法,并进行一些预处理ret = ext4_write_checks(iocb, from);if (ret <= 0)goto out;// 设置当前进程的 backing_dev_info 为 inode 对应的设备current->backing_dev_info = inode_to_bdi(inode);// 执行通用的写入操作,将数据写入到文件中ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos);// 清除当前进程的 backing_dev_infocurrent->backing_dev_info = NULL;out:// 解锁 inodeinode_unlock(inode);// 如果写入操作成功,则更新文件位置,并同步写入数据if (likely(ret > 0)) {iocb->ki_pos += ret;ret = generic_write_sync(iocb, ret);}// 返回写入的字节数或者错误码return ret;
}

写入的执行最后还是回到了VFS。generic_perform_write处理从用户空间到文件的写入数据,方法是遍历数据块、与页面缓存交互以定位或分配页面、将数据复制到这些页面、更新文件的元数据、将页面标记为脏页面以便稍后回写到存储,以及确保整个过程中的数据完整性和错误处理。

ssize_t generic_perform_write(struct file *file,struct iov_iter *i, loff_t pos)
{struct address_space *mapping = file->f_mapping;const struct address_space_operations *a_ops = mapping->a_ops;long status = 0;ssize_t written = 0;unsigned int flags = 0;do {struct page *page;unsigned long offset;	/* Offset into pagecache page */unsigned long bytes;	/* Bytes to write to page */size_t copied;		/* Bytes copied from user */void *fsdata;offset = (pos & (PAGE_SIZE - 1));bytes = min_t(unsigned long, PAGE_SIZE - offset,iov_iter_count(i));again:/** Bring in the user page that we will copy from _first_.* Otherwise there's a nasty deadlock on copying from the* same page as we're writing to, without it being marked* up-to-date.** Not only is this an optimisation, but it is also required* to check that the address is actually valid, when atomic* usercopies are used, below.*/// 错误处理和信号检测if (unlikely(iov_iter_fault_in_readable(i, bytes))) {status = -EFAULT;break;}if (fatal_signal_pending(current)) {status = -EINTR;break;}// 负责将目标文件对应的页加载到内存中,准备好缓冲区以便写入数据。这个函数可能会涉及到文件系统特定的逻辑,例如预分配块或者处理写入锁。status = a_ops->write_begin(file, mapping, pos, bytes, flags,&page, &fsdata);if (unlikely(status < 0))break;// 如果页面映射到用户空间并且可能被写入,则确保缓存一致性,以防止缓存中的旧数据与内存中的新数据冲突。if (mapping_writably_mapped(mapping))flush_dcache_page(page);// 从用户空间缓冲区复制数据到内核页面缓存copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);flush_dcache_page(page);// 负责处理写操作后的收尾工作,例如更新文件大小、标记页面脏、解除页面锁定等status = a_ops->write_end(file, mapping, pos, bytes, copied,page, fsdata);if (unlikely(status < 0))break;copied = status;cond_resched();iov_iter_advance(i, copied);if (unlikely(copied == 0)) {/** If we were unable to copy any data at all, we must* fall back to a single segment length write.** If we didn't fallback here, we could livelock* because not all segments in the iov can be copied at* once without a pagefault.*/bytes = min_t(unsigned long, PAGE_SIZE - offset,iov_iter_single_seg_count(i));goto again;}pos += copied;written += copied;balance_dirty_pages_ratelimited(mapping);} while (iov_iter_count(i));return written ? written : status;
}
EXPORT_SYMBOL(generic_perform_write);
ext4 write begin

ext4_write_begin处理将数据写入文件的准备工作。确保正确设置数据结构和状态,以便实际的数据写入操作顺利进行

锁定inode、在页面缓存中分配页面以及初始化日志事务以确保文件系统的一致性、确定需要修改的特定块,并在必要时从磁盘读取任何现有数据,以避免覆盖块的未初始化部分。

static int ext4_write_begin(struct file *file, struct address_space *mapping,loff_t pos, unsigned len, unsigned flags,struct page **pagep, void **fsdata)
{struct inode *inode = mapping->host;int ret, needed_blocks;handle_t *handle;int retries = 0;struct page *page;pgoff_t index;unsigned from, to;// 检查文件系统是否被强制关闭if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))return -EIO;// 记录写入操作的跟踪信息trace_ext4_write_begin(inode, pos, len, flags);/** Reserve one block more for addition to orphan list in case* we allocate blocks but write fails for some reason*/// 计算写操作所需的块数,包括一个额外的块用于孤儿列表(orphan list)的添加needed_blocks = ext4_writepage_trans_blocks(inode) + 1;// 计算写入位置的页索引、起始偏移量和结束偏移量index = pos >> PAGE_SHIFT;from = pos & (PAGE_SIZE - 1);to = from + len;// 如果文件可能包含内联数据,尝试写入内联数据if (ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA)) {ret = ext4_try_to_write_inline_data(mapping, inode, pos, len,flags, pagep);if (ret < 0)return ret;if (ret == 1)return 0;}// 进行事务处理之前需要先调用 grab_cache_page_write_begin() 获取页面。这样做可以避免在高系统负载或内存压力下造成的长时间等待,同时允许更灵活的内存分配,从而减少潜在的死锁风险。这种策略有助于确保文件系统写入操作的效率和可靠性
retry_grab:// 获取要写入的缓存页。如果获取失败page = grab_cache_page_write_begin(mapping, index, flags);if (!page)return -ENOMEM;unlock_page(page);retry_journal:// 开始一个新的Ext4事务handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE, needed_blocks);if (IS_ERR(handle)) {put_page(page);return PTR_ERR(handle);}// 锁定页面并确保页面稳定。如果页面映射发生变化,重新获取页面lock_page(page);if (page->mapping != mapping) {/* The page got truncated from under us */unlock_page(page);put_page(page);ext4_journal_stop(handle);goto retry_grab;}/* In case writeback began while the page was unlocked */wait_for_stable_page(page);// 根据文件系统状态选择写入方法,并执行实际的写入操作
#ifdef CONFIG_FS_ENCRYPTIONif (ext4_should_dioread_nolock(inode))ret = ext4_block_write_begin(page, pos, len,ext4_get_block_unwritten);elseret = ext4_block_write_begin(page, pos, len,ext4_get_block);
#elseif (ext4_should_dioread_nolock(inode))ret = __block_write_begin(page, pos, len,ext4_get_block_unwritten);elseret = __block_write_begin(page, pos, len, ext4_get_block);
#endifif (!ret && ext4_should_journal_data(inode)) {ret = ext4_walk_page_buffers(handle, page_buffers(page),from, to, NULL,do_journal_get_write_access);}// 处理写入过程中出现的错误。如果需要重试分配块,重新开始事务if (ret) {bool extended = (pos + len > inode->i_size) &&!ext4_verity_in_progress(inode);unlock_page(page);/** __block_write_begin may have instantiated a few blocks* outside i_size.  Trim these off again. Don't need* i_size_read because we hold i_mutex.** Add inode to orphan list in case we crash before* truncate finishes*/if (extended && ext4_can_truncate(inode))ext4_orphan_add(handle, inode);ext4_journal_stop(handle);if (extended) {ext4_truncate_failed_write(inode);/** If truncate failed early the inode might* still be on the orphan list; we need to* make sure the inode is removed from the* orphan list in that case.*/if (inode->i_nlink)ext4_orphan_del(NULL, inode);}if (ret == -ENOSPC &&ext4_should_retry_alloc(inode->i_sb, &retries))goto retry_journal;put_page(page);return ret;}*pagep = page;return ret;
}

block_write_begin通过映射页中必要的块来准备要写的页。遍历每个块,确保将其映射并标记为最新的,如果有必要,还会对需要从磁盘读取的块发起读取,以避免覆盖未初始化的数据

// 	•	page: 需要写入数据的页面。
//	•	pos: 写操作的起始位置。
//	•	len: 写入数据的长度。
//	•	get_block: 用于映射逻辑块号到物理块号的回调函数。
//	•	iomap: I/O 映射结构体,用于描述 I/O 操作。
int __block_write_begin(struct page *page, loff_t pos, unsigned len,get_block_t *get_block)
{return __block_write_begin_int(page, pos, len, get_block, NULL);
}
EXPORT_SYMBOL(__block_write_begin);int __block_write_begin_int(struct page *page, loff_t pos, unsigned len,get_block_t *get_block, struct iomap *iomap)
{unsigned from = pos & (PAGE_SIZE - 1);unsigned to = from + len;struct inode *inode = page->mapping->host;unsigned block_start, block_end;sector_t block;int err = 0;unsigned blocksize, bbits;struct buffer_head *bh, *head, *wait[2], **wait_bh=wait;BUG_ON(!PageLocked(page));BUG_ON(from > PAGE_SIZE);BUG_ON(to > PAGE_SIZE);BUG_ON(from > to);// 为页面分配缓冲头,并设置缓冲区大小和块大小的位数head = create_page_buffers(page, inode, 0);blocksize = head->b_size;bbits = block_size_bits(blocksize);block = (sector_t)page->index << (PAGE_SHIFT - bbits);// 遍历页面的每个缓冲头(buffer head),处理每个块for(bh = head, block_start = 0; bh != head || !block_start;block++, block_start=block_end, bh = bh->b_this_page) {block_end = block_start + blocksize;if (block_end <= from || block_start >= to) {if (PageUptodate(page)) {if (!buffer_uptodate(bh))set_buffer_uptodate(bh);}continue;}if (buffer_new(bh))clear_buffer_new(bh);// 如果缓冲区尚未映射,则调用get_block或者iomap_to_bh进行块映射if (!buffer_mapped(bh)) {WARN_ON(bh->b_size != blocksize);if (get_block) {err = get_block(inode, block, bh, 1);if (err)break;} else {iomap_to_bh(inode, block, bh, iomap);}if (buffer_new(bh)) {clean_bdev_bh_alias(bh);if (PageUptodate(page)) {clear_buffer_new(bh);set_buffer_uptodate(bh);mark_buffer_dirty(bh);continue;}if (block_end > to || block_start < from)zero_user_segments(page,to, block_end,block_start, from);continue;}}if (PageUptodate(page)) {if (!buffer_uptodate(bh))set_buffer_uptodate(bh);continue; }// 如果缓冲区未更新且未延迟,也未写入,则从磁盘读取块数据if (!buffer_uptodate(bh) && !buffer_delay(bh) &&!buffer_unwritten(bh) &&(block_start < from || block_end > to)) {ll_rw_block(REQ_OP_READ, 0, 1, &bh);*wait_bh++=bh;}}/** If we issued read requests - let them complete.*/// 等待所有读取操作完成while(wait_bh > wait) {wait_on_buffer(*--wait_bh);if (!buffer_uptodate(*wait_bh))err = -EIO;}if (unlikely(err))page_zero_new_buffers(page, from, to);return err;
}
ext4 write end

ext4_write_end对页的数据写入做收尾工作。

如果写入扩展了文件,则更新inode大小,必要时将inode标记为脏的,并处理任何清理,包括处理日志事务,如果写入部分失败,则截断超出新文件大小的未初始化块。保证写操作后数据的完整性和一致性。

/** We need to pick up the new inode size which generic_commit_write gave us* `file' can be NULL - eg, when called from page_symlink().** ext4 never places buffers on inode->i_mapping->private_list.  metadata* buffers are managed internally.*/
static int ext4_write_end(struct file *file,struct address_space *mapping,loff_t pos, unsigned len, unsigned copied,struct page *page, void *fsdata)
{handle_t *handle = ext4_journal_current_handle();struct inode *inode = mapping->host;loff_t old_size = inode->i_size;int ret = 0, ret2;int i_size_changed = 0;int inline_data = ext4_has_inline_data(inode);bool verity = ext4_verity_in_progress(inode);trace_ext4_write_end(inode, pos, len, copied);// 包含内联数据,处理内联数据写入,否则进行块写入if (inline_data) {ret = ext4_write_inline_data_end(inode, pos, len,copied, page);if (ret < 0) {unlock_page(page);put_page(page);goto errout;}copied = ret;} elsecopied = block_write_end(file, mapping, pos,len, copied, page, fsdata);/** it's important to update i_size while still holding page lock:* page writeout could otherwise come in and zero beyond i_size.** If FS_IOC_ENABLE_VERITY is running on this inode, then Merkle tree* blocks are being written past EOF, so skip the i_size update.*/if (!verity)i_size_changed = ext4_update_inode_size(inode, pos + copied);unlock_page(page);put_page(page);// 如果旧文件大小小于写入位置,且没有正在进行的文件校验扩展操作,更新页面缓存的文件大小if (old_size < pos && !verity)pagecache_isize_extended(inode, old_size, pos);/** Don't mark the inode dirty under page lock. First, it unnecessarily* makes the holding time of page lock longer. Second, it forces lock* ordering of page lock and transaction start for journaling* filesystems.*/// 如果文件大小发生变化或包含内联数据,标记inode为脏if (i_size_changed || inline_data)ext4_mark_inode_dirty(handle, inode);// 如果写入位置加上写入长度超过了文件大小,并且文件系统允许截断,添加inode到孤儿列表if (pos + len > inode->i_size && !verity && ext4_can_truncate(inode))/* if we have allocated more blocks and copied* less. We will have blocks allocated outside* inode->i_size. So truncate them*/ext4_orphan_add(handle, inode);
errout:ret2 = ext4_journal_stop(handle);if (!ret)ret = ret2;if (pos + len > inode->i_size && !verity) {ext4_truncate_failed_write(inode);/** If truncate failed early the inode might still be* on the orphan list; we need to make sure the inode* is removed from the orphan list in that case.*/if (inode->i_nlink)ext4_orphan_del(NULL, inode);}return ret ? ret : copied;
}

ext4 direct IO

static ssize_t ext4_dio_write_iter(struct kiocb *iocb, struct iov_iter *from)
{ssize_t ret;size_t count;loff_t offset;handle_t *handle;struct inode *inode = file_inode(iocb->ki_filp);bool extend = false, overwrite = false, unaligned_aio = false;// 锁定inode。if (iocb->ki_flags & IOCB_NOWAIT) {if (!inode_trylock(inode))return -EAGAIN;} else {inode_lock(inode);}// 检查是否支持直接IOif (!ext4_dio_supported(inode)) {inode_unlock(inode);/** Fallback to buffered I/O if the inode does not support* direct I/O.*/return ext4_buffered_write_iter(iocb, from);}// 写入前检查ret = ext4_write_checks(iocb, from);if (ret <= 0) {inode_unlock(inode);return ret;}// 同步未对齐的异步direct IO,防止数据损坏offset = iocb->ki_pos;count = iov_iter_count(from);if (ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS) &&!is_sync_kiocb(iocb) && ext4_unaligned_aio(inode, from, offset)) {unaligned_aio = true;inode_dio_wait(inode);}/** Determine whether the I/O will overwrite allocated and initialized* blocks. If so, check to see whether it is possible to take the* dioread_nolock path.*/// 如果IO对齐且I/O覆盖已分配和初始化的块且 inode 支持无锁直接读取,则设置 overwrite 并降级写锁if (!unaligned_aio && ext4_overwrite_io(inode, offset, count) &&ext4_should_dioread_nolock(inode)) {overwrite = true;downgrade_write(&inode->i_rwsem);}// 检查写操作的结束(offset + count)是否超过了inode的当前磁盘大小,启动一个日志句柄来安全地管理对inode的更改,将该inode添加到孤立列表中,以处理写操作期间可能发生的崩溃,并设置一个标志(extend),表示将扩展inode的大小。然后停止日志记录句柄if (offset + count > EXT4_I(inode)->i_disksize) {handle = ext4_journal_start(inode, EXT4_HT_INODE, 2);if (IS_ERR(handle)) {ret = PTR_ERR(handle);goto out;}ret = ext4_orphan_add(handle, inode);if (ret) {ext4_journal_stop(handle);goto out;}extend = true;ext4_journal_stop(handle);}// 执行直接 I/O 写入ret = iomap_dio_rw(iocb, from, &ext4_iomap_ops, &ext4_dio_write_ops,is_sync_kiocb(iocb) || unaligned_aio || extend);// 如果是扩展操作,需要再次启动一个事务并更新磁盘大小if (extend)ret = ext4_handle_inode_extension(inode, offset, ret, count);out:if (overwrite)inode_unlock_shared(inode);elseinode_unlock(inode);if (ret >= 0 && iov_iter_count(from)) {ssize_t err;loff_t endbyte;// 回退到缓冲IOoffset = iocb->ki_pos;err = ext4_buffered_write_iter(iocb, from);if (err < 0)return err;// 在当前 I/O 操作覆盖的范围内,确保页面缓存中的页面被写入磁盘并失效(即使缓存无效)。这是为了在必要时回退到缓冲 I/O 时,尽量保持直接 I/O 的语义ret += err;endbyte = offset + err - 1;err = filemap_write_and_wait_range(iocb->ki_filp->f_mapping,offset, endbyte);if (!err)invalidate_mapping_pages(iocb->ki_filp->f_mapping,offset >> PAGE_SHIFT,endbyte >> PAGE_SHIFT);}return ret;
}

ext4 BIO与DIO代码有感

ext4 BIO(Buffered IO)与DIO(Direct IO)

  • ext4 BIO与DIO都尝试对inode进行锁定。不同的是DIO还允许无等待,也就是在锁已经被获取的情况下,直接返回

  • BIO经过内核page缓存,而DIO则直接从用户空间写入到设备

  • DIO还确保写入操作覆盖范围内的缓存页面被写入磁盘并失效,以保证直接 I/O 语义,和未对齐的异步直接 I/O 写入,防止数据损坏

Ref

  1. https://elixir.bootlin.com/linux/v5.5-rc2/source

相关文章:

Linux文件数据写入

结构体 fd fd也就是文件描述符&#xff0c;用于标识已经打开的文件、管道、socket等。是进程和内核的桥梁&#xff0c;允许进程执行各种文件操作 struct fd {struct file *file;unsigned int flags; };file Linux内核中表示打开文件的结构体&#xff0c;包含了文件操作所需…...

vue2 中如何使用 vuedraggable 库实现拖拽功能

1.通过 npm 或 yarn 安装 vuedraggable 库 npm install vuedraggableyarn add vuedraggable 2. 引入组件内部使用&#xff0c;以下代码是一个Demo&#xff0c;可直接复制粘贴演示 注意&#xff1a;因项目使用了 vant&#xff0c;需要安装 vant 才能正常运行 <template&g…...

0基础学C++ | 第13天 | 基础知识 | 类 | 对象

目录 前言 封装 封装的意义 struct 和 class 的区别 成员属性设置为私有 前言 众所周知&#xff0c; C是一个面向对象的编程语言&#xff08;面向对象的C语言的特点就是&#xff1a;封装、继、 多态&#xff09;&#xff0c;它与面向过程的C语言不通&#xff0c;对面向…...

Java | Leetcode Java题解之第212题单词搜索II

题目&#xff1a; 题解&#xff1a; class Solution {int[][] dirs {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};public List<String> findWords(char[][] board, String[] words) {Trie trie new Trie();for (String word : words) {trie.insert(word);}Set<String> a…...

Flink面试题总结

一、简单介绍一下 Flink Apache Flink 是一个实时计算框架和分布式处理引擎&#xff0c;用于在无边界和有边界数据流上进行有状态的计算 二、Flink集群有哪些角色&#xff1f;各自有什么作用&#xff1f;&#xff08;flink架构&#xff09; --JobManager&#xff1a; JobManag…...

人工智能与云计算

项目要求 一个简单的集群。您需要在此项目中创建计算机集群。这些机器是 docker 容器。集群管理器是一个 Python 程序。群集的状态将写入文件。 希望通过这个 Python 文件,首先它能够通过获取输入来得到要创建的集群中包含的容器数量,并与用户进行交互(用户可以执行此集群…...

9.(vue3.x+vite)修改el-input,el-data-picker样式

效果预览 二:相关代码 <template><div style="padding: 50px"><el-input placeholder="请输入模型名称" style="width: 260px" /><br /...

java反射和注解

反射 获取class对象的三种方法 ①&#xff1a;Class.forName("全类名"); ②&#xff1a;类名.class ③&#xff1a;对象.getclass(); 代码样例 package com.ithema;public class Main {public static void main(String[] args) throws ClassNotFoundException {//第…...

react_后台管理_项目

目录 1.运行项目 2. 项目结构 ①项目顶部导航栏 ②项目左侧导航栏 ③主页面-路由切换区 本项目使用的是 reacttsscss 技术栈。 1.运行项目 在当前页面顶部下载本项目&#xff0c;解压后使用编辑器打开&#xff0c;然后再终端输入命令&#xff1a; npm i 下载依赖后&am…...

【C语言】使用C语言编写并使用gcc编译动态链接库

【C语言】使用C 语言编写并使用 gcc 编译动态链接库 1.背景2.使用C编写代码3.使用gcc编译代码1.背景 在windows下开发很多程序接口被封装到动态链接库供其它开发者使用。 本博客使用C语言编写并使用gcc 编译 一个动态链接库文件FpdSys.dll; 然后使用C/C++/C#/Python去调用动态…...

使用supportFragmentManager管理多个fragment切换

android studio创建的项目就没有一个简单点的框架&#xff0c;生成的代码都是繁琐而复杂&#xff0c;并且不实用。 国内的页面一般都是TAB页面的比较多&#xff0c;老外更喜欢侧边菜单。 如果我们使用一个activity来创建程序&#xff0c;来用占位符管理多个fragment切换&…...

开源模型应用落地-FastAPI-助力模型交互-WebSocket篇(六)

一、前言 使用 FastAPI 可以帮助我们更简单高效地部署 AI 交互业务。FastAPI 提供了快速构建 API 的能力,开发者可以轻松地定义模型需要的输入和输出格式,并编写好相应的业务逻辑。 FastAPI 的异步高性能架构,可以有效支持大量并发的预测请求,为用户提供流畅的交互体验。此外,F…...

独立开发者系列(17)——MYSQL的常见异常整理

虽然安装MYSQL到本地很简单&#xff0c;但是数据库报错还是经常出现&#xff0c;这个时候&#xff0c;需要我们进行逐步检查与修复。作为我们最常用的开发软件&#xff0c;无论切换php/go/python/node/java&#xff0c;数据库的身影都少不了&#xff0c;对于我们储存数据而言&a…...

【ajax实战02】数据管理网站—验证码登录

一&#xff1a;数据提交&#xff08;提交手机验证码&#xff09; 核心思路整理 利用form-serialize插件&#xff0c;收集对象形式的表单数据后&#xff0c;一并提交给服务器。后得到返回值&#xff0c;进一步操作 基地址&#xff1a; axios.defaults.baseURL http://geek.…...

人工智能在反无人机中的应用介绍

人工智能技术在无人机的发展中扮演着至关重要的角色&#xff0c;这一作用在反无人机技术领域同样显著。随着无人机技术的发展&#xff0c;飞行器具备了微小尺寸、高速机动性&#xff0c;以及可能采用的隐蔽或低空飞行轨迹等特性。这些特性使得传统的人工监视和控制手段面临着重…...

【力扣 - 每日一题】3115. 质数的最大距离(一次遍历、头尾遍历、空间换时间、埃式筛、欧拉筛、打表)Golang实现

原题链接 题目描述 给你一个整数数组 nums。 返回两个&#xff08;不一定不同的&#xff09;质数在 nums 中 下标 的 最大距离。 示例 1&#xff1a; 输入&#xff1a; nums [4,2,9,5,3] 输出&#xff1a; 3 解释&#xff1a; nums[1]、nums[3] 和 nums[4] 是质数。因此答…...

【Gin】项目搭建 一

环境准备 首先确保自己电脑安装了Golang 开始项目 1、初始化项目 mkdir gin-hello; # 创建文件夹 cd gin-hello; # 需要到刚创建的文件夹里操作 go mod init goserver; # 初始化项目&#xff0c;项目名称&#xff1a;goserver go get -u github.com/gin-gonic/gin; # 下载…...

C++ 和C#的差别

首先把眼睛瞪大&#xff0c;然后憋住一口气&#xff0c;读下去&#xff1a; 1、CPP 就是C plus plus的缩写&#xff0c;中国大陆的程序员圈子中通常被读做"C加加"&#xff0c;而西方的程序员通常读做"C plus plus"&#xff0c;它是一种使用非常广泛的计算…...

Vue2组件传值(通信)的方式

目录 1.父传后代 ( 后代拿到了父的数据 )1. 父组件引入子组件&#xff0c;绑定数据2. 子组件直接使用父组件的数据3. 依赖注入(使用 provide/inject API)1.在祖先组件中使用 provide2.在后代组件中使用 inject 2.后代传父 &#xff08;父拿到了后代的数据&#xff09;1. 子组件…...

【数据结构 - 时间复杂度和空间复杂度】

文章目录 <center>时间复杂度和空间复杂度算法的复杂度时间复杂度大O的渐进表示法常见时间复杂度计算举例 空间复杂度实例 时间复杂度和空间复杂度 算法的复杂度 算法在编写成可执行程序后&#xff0c;运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏&…...

telegram支付

今天开始接入telegram支付,参考教程这个是telegram的官方说明,详细介绍了机器人支付API。 文章公开地址 新建机器人 因为支付是一个单独的系统,所以在做支付的时候单独创建了一个bot,没有用之前的bot了,特意这样将其分开。创建bot的方法和之前不变,这里不过多介绍。 获…...

elasticsearch-6.8.23的集群搭建过程

三个节点的 ElasticSearch 集群搭建步骤 准备三台机器&#xff1a;28.104.87.98、28.104.87.100、28.104.87.101 和 ElasticSearch 的安装包 elasticsearch-6.8.23.tar.gz ----------------------------- 28.104.87.98&#xff0c;使用 root 用户操作 ----------------------…...

javascript输出语法

javascript输出有三种方式 一种是弹窗输出&#xff0c;就是网页弹出一个对话框&#xff0c;弹出输出内容 语法是aler(内容) 示例代码如下 <body> <script> alert(你好); </script> </body> 这段代码运行后网页会出现一个对话框&#xff0c;弹出你…...

仓库管理系统26--权限设置

原创不易&#xff0c;打字不易&#xff0c;截图不易&#xff0c;多多点赞&#xff0c;送人玫瑰&#xff0c;留有余香&#xff0c;财务自由明日实现 1、权限概述 在应用软件中&#xff0c;通常将软件的功能分为若干个子程序&#xff0c;通过主程序调用。那么&#xff0c;通过…...

d3dx9_43.dll丢失怎么解决?d3dx9_43.dll怎么安装详细教程

在使用计算机中&#xff0c;如果遇到d3dx9_43.dll丢失或许找不到d3dx9_43.dll无法运行打开软件怎么办&#xff1f;这个是非常常见问题&#xff0c;下面我详细介绍一下d3dx9_43.dll是什么文件与d3dx9_43.dll的各种问题以及d3dx9_43.dll丢失的多个解决方法&#xff01; 一、d3dx9…...

[C++] 退出清理函数解读(exit、_exit、abort、atexit)

说明&#xff1a;在C中&#xff0c;exit、_exit&#xff08;或_Exit&#xff09;、abort和atexit是用于控制程序退出和清理的标准库函数。下面是对这些函数的详细解读&#xff1a; exit 函数原型&#xff1a;void exit(int status);作用&#xff1a;exit函数用于正常退出程序…...

代码随想录(回溯)

组合&#xff08;Leetcode77&#xff09; 思路 用递归每次遍历从1-n得数&#xff0c;然后list来记录是不是组合到k个了&#xff0c;然后这个每次for循环的开始不能和上一个值的开始重复&#xff0c;所以设置个遍历开始索引startindex class Solution {static List<List<…...

编译原理1

NFA&DFA 在正规式的等价证明可以借助正规集&#xff0c;也可以通过有限自动机DFA来证明等价&#xff0c;以下例题是针对DFA证明正规式的等价&#xff0c;主要步骤是①NFA&#xff1b;②状态转换表&#xff1b; ③状态转换矩阵&#xff1b; ④化简DFA&#xff1b; 文法和语…...

【信息系统项目管理师知识点速记】组织通用管理:流程管理

23.2 流程管理 通过流程视角能够真正看清楚组织系统的本质与内在联系,理顺流程能够理顺整个组织系统。流程是组织运行体系的框架基础,流程框架的质量影响和决定了整个组织运行体系的质量。把流程作为组织运行体系的主线,配备满足流程运作需要的资源,并构建与流程框架相匹配…...

前端 JS 经典:箭头函数的意义

箭头函数是为了消除函数的二义性。 1. 二义性 函数的二义性指函数有不同的两种用法&#xff0c;就造成了二义性&#xff0c;函数的两种用法&#xff1a;1. 指令序列。2. 构造器 1.1 指令序列 就是调用函数&#xff0c;相当于将函数内部的代码再从头执行一次。 1.2 构造器 …...

Java List操作详解及常用方法

Java List操作详解及常用方法 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 什么是Java List&#xff1f; Java中的List是一种动态数组&#xff0c;它允许存…...

《mysql篇》--查询(进阶)

目录 将查询结果作为插入数据 聚合查询 聚合函数 count sum group by子句 having 联合查询 笛卡尔积 多表查询 join..on实现多表查询 内连接 外连接 自连接 子查询 合并查询 将查询结果作为插入数据 Insert into 表2 select * from 表1//将表1的查询数据插入…...

数据库-MySQL 实战项目——书店图书进销存管理系统数据库设计与实现(附源码)

一、前言 该项目非常适合MySQL入门学习的小伙伴&#xff0c;博主提供了源码、数据和一些查询语句&#xff0c;供大家学习和参考&#xff0c;代码和表设计有什么不恰当还请各位大佬多多指点。 所需环境 MySQL可视化工具&#xff1a;navicat&#xff1b; 数据库&#xff1a;MySq…...

eNSP中WLAN的配置和使用

一、基础配置 1.拓扑图 2.VLAN和IP配置 a.R1 <Huawei>system-view [Huawei]sysname R1 GigabitEthernet 0/0/0 [R1-GigabitEthernet0/0/0]ip address 200.200.200.200 24 b.S1 <Huawei>system-view [Huawei]sysname S1 [S1]vlan 100 [S1-vlan100]vlan 1…...

<sa8650>QCX ID16_UsecaseRawLiteAuto 使用详解

<sa8650>QCX ID16_UsecaseRawLiteAuto 使用详解 一、前言二、ID16_UsecaseRawLiteAuto拓扑图三、UsecaseRawLiteAuto拓扑图 解析3.1 camxUsecaseRawLiteAuto.xml3.2 camxRawLiteAuto.xml四、测试一、前言 我们在使用QCX时,如果由于使用的摄像头自带了ISP,那么可能不需要使…...

为什么3d重制变换模型会变形?---模大狮模型网

在当今数字技术飞速发展的时代&#xff0c;3D建模和动画制作已经成为影视、游戏和虚拟现实中不可或缺的一部分。然而&#xff0c;即使在高级的3D软件中&#xff0c;重制(rigging)和变换(transformation)过程中仍然会面临一个普遍的问题——模型变形。这种变形可能导致动画效果不…...

ElasticSearch中的BM25算法实现原理及应用分析

文章目录 一、引言二、BM25算法实现原理BM25算法的实现原理1. 词频&#xff08;TF&#xff09;&#xff1a;2. 逆文档频率&#xff08;IDF&#xff09;&#xff1a;3. 长度归一化&#xff1a;4. BM25评分公式&#xff1a; BM25算法示例 三、BM25算法在ElasticSearch中的应用分析…...

web权限到系统权限 内网学习第一天 权限提升 使用手工还是cs???msf可以不??

现在开始学习内网的相关的知识了&#xff0c;我们在拿下web权限过后&#xff0c;我们要看自己拿下的是什么权限&#xff0c;可能是普通的用户权限&#xff0c;这个连添加用户都不可以&#xff0c;这个时候我们就要进行权限提升操作了。 权限提升这点与我们后门进行内网渗透是乘…...

ros1仿真导航机器人 hector_mapping gmapping

仅为学习记录和一些自己的思考&#xff0c;不具有参考意义。 1 hector_mapping 建图过程 &#xff08;1&#xff09;gazebo仿真 roslaunch why_simulation why_slam.launch <launch><!-- We resume the logic in empty_world.launch, changing only the name of t…...

嵌入式实验---实验五 串口数据接收实验

一、实验目的 1、掌握STM32F103串口数据接收程序设计流程&#xff1b; 2、熟悉STM32固件库的基本使用。 二、实验原理 1、STM32F103R6能通过查询中断方式接收数据&#xff0c;每接收到一个字节&#xff0c;立即向对方发送一个相同内容的字节&#xff0c;并把该字节的十六进…...

ubuntu 22.04下编译安装glog共享库

笔者是完美主义者&#xff0c;在编译opencv4.9时,有个有关glog的warn&#xff0c;就下载编译google的glog库并把它编译成shared libaray。重新编译opencv4.9时&#xff0c;该warn解除。现把编译安装glog过程记录&#xff0c;以备后查。 以下操作全程以root身份或sudo执行。 cd…...

Linux环境安装配置nginx服务流程

Linux环境的Centos、麒麟、统信操作系统安装配置nginx服务流程操作&#xff1a; 1、官网下载 下载地址 或者通过命令下载 wget http://nginx.org/download/nginx-1.20.2.tar.gz 2、上传到指定的服务器并解压 tar -zxvf nginx-1.20.1.tar.gzcd nginx-1.20.1 3、编译并安装到…...

设计模式-模板模式

简介 模板方法模式是一种行为设计模式&#xff0c;它在父类中定义了一个操作的算法框架&#xff0c;允许子类在不改变算法结构的情况下重定义算法的某些步骤。这种模式是基于继承的&#xff0c;通过抽象类将通用的代码抽取到超类中&#xff0c;同时通过具体类实现或者改写算法…...

物理删除和逻辑删除区别

物理删除和逻辑删除是数据库管理中针对记录删除操作的两种不同方式&#xff0c;它们的主要区别在于数据的实际处理和后续影响&#xff1a; 物理删除&#xff1a; 操作实质&#xff1a;物理删除会将数据记录从数据库表中彻底移除&#xff0c;包括记录所占的磁盘空间都会被释放。…...

C# 警告 warning MSB3884: 无法找到规则集文件“MinimumRecommendedRules.ruleset”

警告 warning MSB3884: 无法找到规则集文件“MinimumRecommendedRules.ruleset” C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\amd64\Microsoft.CSharp.CurrentVersion.targets(129,9): warning MSB3884: 无法找到规则集文件“MinimumRe…...

Lua网站开发之文件表单上传

这个代码示例演示如何上传文件或图片&#xff0c;获取上传信息及保存文件到本地。 local fw require("fastweb") local request require("fastweb.request") local response require("fastweb.response") local cjson require("cjson&q…...

千益畅行,旅游卡,如何赚钱?

​ 赚钱这件事情&#xff0c;只有自己努力执行才会有结果。生活中没有幸运二字&#xff0c;每个光鲜亮丽的背后&#xff0c;都是不为人知的付出&#xff01; #旅游卡服务#...

Element-plus点击当前行之后获取数据显示跟随行数据

要实现点击当前行后&#xff0c;在当前行的下方显示数据&#xff0c;可以通过以下步骤来实现&#xff1a; 在表格的行点击事件中获取当前点击行的位置信息。根据位置信息动态计算并设置需要显示数据区域的位置。 下面是一个更新后的示例代码&#xff0c;演示如何在 Element-P…...

Docker与微服务实战2022 尚

Docker与微服务实战2022 尚硅谷讲师:周阳 1. 基础篇(零基小白) 1 1.1. Docker简介 2 1.2. Docker安装 15 1.3. Docker常用命令 29 1.4. Docker镜像 43 1.5. 本地镜像发布到阿里云 50 1.6. 本地镜像发布到私有库 57 1.7. Docker容器数据卷 64 1.8. Docker常规安装简介 …...

Spring @Cacheable缓存注解用法说明

注解Cacheable 是 Spring 框架中用于缓存数据的方法或类的注解。通过使用这个注解&#xff0c;你可以避免重复计算和重复获取数据&#xff0c;从而提高应用程序的性能。 基本用法 引入依赖 确保在你的项目中引入了 Spring Cache 相关的依赖。如果你使用的是 Spring Boot&…...