Linux 读文件 - readahead预读算法

2023-10-31

顺序读场景

intmain

{

charc[ 4096];

intin = -1;

in = open( "news.txt", O_RDONLY);

intindex= 0;

while(read(in, &c, 4096) == 4096)

{

printf( "index: %d,len: %ld.\n",index, strlen(c));

memset(c, 0, sizeof(c));

index++;

}

数据结构

/*
 * Track a single file's readahead state
 */
struct file_ra_state {
	pgoff_t start;			/* where readahead started */
	unsigned int size;		/* # of readahead pages */
	unsigned int async_size;	/* do asynchronous readahead when
					   there are only # of pages ahead */

	unsigned int ra_pages;		/* Maximum readahead window */
	unsigned int mmap_miss;		/* Cache miss stat for mmap accesses */
	loff_t prev_pos;		/* Cache last read() position */
};

start: 开始预读的数据页索引,是指相对文件内的index。

size : 一共要预读多个少页面。

async_size: 如果当前预读的“存货”只剩async_size时,就会触发async readahead,这个值很重要,控着async readahead的时机。异步读触发时类似网络协议栈的滑动窗口,窗口会移动,也就是file_ra_state字段会更新,具体如何更新见下文。这里async readahead就是指generic_file_buffered_read函数中的:page_cache_async_readahead函数调用

Linux read的核心函数generic_file_buffered_read_nginux的博客-CSDN博客

ra_page: readahead窗口的最大值,类似网络协议栈滑动窗口,这个窗口有最大限制。

prev_pos : 上次读的postion,文件内偏移,主义单位时bytes。

注意:PageReadahead的page非常重要,因为一旦应用程序读取数据到这个page,就要触发异步预读(page_cache_async_readahead)

数据结构初始化:

代码:mm/readahead.c: ondemand_readahead(预读算法的实现函数)


/*
 * A minimal readahead algorithm for trivial sequential/random reads.
 */
static void ondemand_readahead(struct address_space *mapping,
		struct file_ra_state *ra, struct file *filp,
		bool hit_readahead_marker, pgoff_t index,
		unsigned long req_size)
{
	struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
	unsigned long max_pages = ra->ra_pages;
	unsigned long add_pages;
	pgoff_t prev_index;

	/*
	 * If the request exceeds the readahead window, allow the read to
	 * be up to the optimal hardware IO size
	 */
	if (req_size > max_pages && bdi->io_pages > max_pages)
		max_pages = min(req_size, bdi->io_pages);

	/*
	 * start of file
	 */
	if (!index)
		goto initial_readahead;

    ...

initial_readahead:
	ra->start = index;
    //req_size是指预读请求的大小数据页数量(每个数据页4K)
	ra->size = get_init_ra_size(req_size, max_pages);
	ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;

    ...
    //提交文件读取,触发io预读
	ra_submit(ra, mapping, filp);
}

/*
 * Set the initial window size, round to next power of 2 and square
 * for small size, x 4 for medium, and x 2 for large
 * for 128k (32 page) max ra
 * 1-8 page = 32k initial, > 8 page = 128k initial
 */

//第一个参数:请求读取的数据页数量,比如目前场景每次读取4K字节,size = 1
static unsigned long get_init_ra_size(unsigned long size, unsigned long max)
{
	unsigned long newsize = roundup_pow_of_two(size);

    //每次读取小于4K bytes, 预读大小是请求大小的4被,比如read(4096), newsize = 1 * 4 = 4;
	if (newsize <= max / 32)
		newsize = newsize * 4;

    //中等读取大小,即1pages < ra <= 8pages,预读大小设置成请求大小的2倍
    //比如read(4K) newsize = 4 * 2 = 8
	else if (newsize <= max / 4)
		newsize = newsize * 2;
    //大的读取大小,即每次读取大于8 pages,比如read(64K) newsize = max = 32 pages
	else
		newsize = max;

	return newsize;
}

根据《Linux read的核心函数generic_file_buffered_read_nginux的博客-CSDN博客》文章我们知道,顺序读取大概调用逻辑:

1. 触发同步预读(sync readahead)

2.异步异步读取(async readahead)

首次同步预读 - sync readahead

调用路径:generic_file_buffered_read

                         -->page_cache_sync_readahead

                                -->ondemand_readahead : intial_readahead

                                      --->ra_submit 向block layer请求io预读

首次文件头读进入ondemand_readahead的initial_readahead逻辑,计算file_ra_state值:

ra->start = 0; 因为是从文件头开始读取

ra->size = 4;

ra->async_size = 3;

ra->ra_pages = 32;

ra->prev_pos = -1;

根据ondemand_readahead中initial_readahead label处逻辑看,ra->size是由get_init_ra_size函数计算,该函数第一个参数是应用read的数据页(每个数据页4K)的数量,该场景每次读取4K bytes,相当于调用get_init_ra_size(1,32)返回4。

ra_submit向block layer请求io预读

/*
 * Submit IO for the read-ahead request in file_ra_state.
 */
static inline void ra_submit(struct file_ra_state *ra,
		struct address_space *mapping, struct file *filp)
{
	__do_page_cache_readahead(mapping, filp,
			ra->start, ra->size, ra->async_size);
}

/*
 * __do_page_cache_readahead() actually reads a chunk of disk.  It allocates
 * the pages first, then submits them for I/O. This avoids the very bad
 * behaviour which would occur if page allocations are causing VM writeback.
 * We really don't want to intermingle reads and writes like that.
 */
void __do_page_cache_readahead(struct address_space *mapping,
		struct file *file, pgoff_t index, unsigned long nr_to_read,
		unsigned long lookahead_size)
{
	struct inode *inode = mapping->host;
	loff_t isize = i_size_read(inode);
	pgoff_t end_index;	/* The last page we want to read */

	if (isize == 0)
		return;

	end_index = (isize - 1) >> PAGE_SHIFT;
	if (index > end_index)
		return;
	/* Don't read past the page containing the last byte of the file */
	if (nr_to_read > end_index - index)
		nr_to_read = end_index - index + 1;

	page_cache_readahead_unbounded(mapping, file, index, nr_to_read,
			lookahead_size);
}

static inline gfp_t readahead_gfp_mask(struct address_space *x)
{
	return mapping_gfp_mask(x) | __GFP_NORETRY | __GFP_NOWARN;
}


/**
 * page_cache_readahead_unbounded - Start unchecked readahead.
 * @mapping: File address space.
 * @file: This instance of the open file; used for authentication.
 * @index: First page index to read.
 * @nr_to_read: The number of pages to read.
 * @lookahead_size: Where to start the next readahead.
 *
 * This function is for filesystems to call when they want to start
 * readahead beyond a file's stated i_size.  This is almost certainly
 * not the function you want to call.  Use page_cache_async_readahead()
 * or page_cache_sync_readahead() instead.
 *
 * Context: File is referenced by caller.  Mutexes may be held by caller.
 * May sleep, but will not reenter filesystem to reclaim memory.
 */
void page_cache_readahead_unbounded(struct address_space *mapping,
		struct file *file, pgoff_t index, unsigned long nr_to_read,
		unsigned long lookahead_size)
{
	LIST_HEAD(page_pool);

    //注意这里添加了__GFP_NORETRY | __GFP_NOWARN,page内存申请不会进入慢路径
	gfp_t gfp_mask = readahead_gfp_mask(mapping);
	struct readahead_control rac = {
		.mapping = mapping,
		.file = file,
		._index = index,
	};
	unsigned long i;

	/*
	 * Partway through the readahead operation, we will have added
	 * locked pages to the page cache, but will not yet have submitted
	 * them for I/O.  Adding another page may need to allocate memory,
	 * which can trigger memory reclaim.  Telling the VM we're in
	 * the middle of a filesystem operation will cause it to not
	 * touch file-backed pages, preventing a deadlock.  Most (all?)
	 * filesystems already specify __GFP_NOFS in their mapping's
	 * gfp_mask, but let's be explicit here.
	 */
	unsigned int nofs = memalloc_nofs_save();

	/*
	 * Preallocate as many pages as we will need.
	 */
	for (i = 0; i < nr_to_read; i++) {
		struct page *page = xa_load(&mapping->i_pages, index + i);

		BUG_ON(index + i != rac._index + rac._nr_pages);

		if (page && !xa_is_value(page)) {
			/*
			 * Page already present?  Kick off the current batch
			 * of contiguous pages before continuing with the
			 * next batch.  This page may be the one we would
			 * have intended to mark as Readahead, but we don't
			 * have a stable reference to this page, and it's
			 * not worth getting one just for that.
			 */
			read_pages(&rac, &page_pool, true);
			continue;
		}

		page = __page_cache_alloc(gfp_mask);
		if (!page)
			break;
		if (mapping->a_ops->readpages) {
			page->index = index + i;
			list_add(&page->lru, &page_pool);
		} else if (add_to_page_cache_lru(page, mapping, index + i,
					gfp_mask) < 0) {
			put_page(page);
			read_pages(&rac, &page_pool, true);
			continue;
		}
        //设置PageReadahead标志,非常重要
		if (i == nr_to_read - lookahead_size)
			SetPageReadahead(page);
		rac._nr_pages++;
	}

	/*
	 * Now start the IO.  We ignore I/O errors - if the page is not
	 * uptodate then the caller will launch readpage again, and
	 * will then handle the error.
	 */
	read_pages(&rac, &page_pool, false);
	memalloc_nofs_restore(nofs);
}

上面由于预读了4个页面,所以下次generic_file_buffered_read for(;;)循环中find_get_page会找到cache page(因为被预读进cache中了),循环继续,每次循环增加index值,由于index增加,就会触发PageReadahead(page),进而调用page_cache_async_readahead:

这种设计是因为不能一直预读,因为预读失败会受到惩罚(浪费内存),而是要根据应用顺序读取到一定程度才进行新的预读,这个时机就是应用读取到PageReadahead(page)对应的page。page_cache_async_readahead-->ondemand_readahead触发新的“异步”预读:


/*
 * A minimal readahead algorithm for trivial sequential/random reads.
 */
static void ondemand_readahead(struct address_space *mapping,
		struct file_ra_state *ra, struct file *filp,
		bool hit_readahead_marker, pgoff_t index,
		unsigned long req_size)
{
	struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
	unsigned long max_pages = ra->ra_pages;
	unsigned long add_pages;
	pgoff_t prev_index;

	/*
	 * If the request exceeds the readahead window, allow the read to
	 * be up to the optimal hardware IO size
	 */
	if (req_size > max_pages && bdi->io_pages > max_pages)
		max_pages = min(req_size, bdi->io_pages);

	/*
	 * start of file
	 */
	if (!index)
		goto initial_readahead;

	/*
	 * It's the expected callback index, assume sequential access.
	 * Ramp up sizes, and push forward the readahead window.
	 */
     //@1
	if ((index == (ra->start + ra->size - ra->async_size) ||
	     index == (ra->start + ra->size))) {
		ra->start += ra->size;
		ra->size = get_next_ra_size(ra, max_pages);
		ra->async_size = ra->size;
		goto readit;
	}

	/*
	 * Hit a marked page without valid readahead state.
	 * E.g. interleaved reads.
	 * Query the pagecache for async_size, which normally equals to
	 * readahead size. Ramp it up and use it as the new readahead size.
	 */
    //@2 没有命中@1,比如没有完整顺序读的情况,刚好跳过了@1中的条件
	if (hit_readahead_marker) {
		pgoff_t start;

		rcu_read_lock();
		start = page_cache_next_miss(mapping, index + 1, max_pages);
		rcu_read_unlock();

		if (!start || start - index > max_pages)
			return;

		ra->start = start;
		ra->size = start - index;	/* old async_size */
		ra->size += req_size;
		ra->size = get_next_ra_size(ra, max_pages);
		ra->async_size = ra->size;
		goto readit;
	}

	/*
	 * oversize read
	 */
	if (req_size > max_pages)
		goto initial_readahead;

	/*
	 * sequential cache miss
	 * trivial case: (index - prev_index) == 1
	 * unaligned reads: (index - prev_index) == 0
	 */
	prev_index = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
	if (index - prev_index <= 1UL)
		goto initial_readahead;

	/*
	 * Query the page cache and look for the traces(cached history pages)
	 * that a sequential stream would leave behind.
	 */
	if (try_context_readahead(mapping, ra, index, req_size, max_pages))
		goto readit;

	/*
	 * standalone, small random read
	 * Read as is, and do not pollute the readahead state.
	 */
    //@3随机读取,倒数第二个参数代表只读取用户要求数量的page数量,最后一个参数0代表不进行
    //预读
	__do_page_cache_readahead(mapping, filp, index, req_size, 0);
	return;

initial_readahead:
	ra->start = index;
	ra->size = get_init_ra_size(req_size, max_pages);
	ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;

readit:
	/*
	 * Will this read hit the readahead marker made by itself?
	 * If so, trigger the readahead marker hit now, and merge
	 * the resulted next readahead window into the current one.
	 * Take care of maximum IO pages as above.
	 */
	if (index == ra->start && ra->size == ra->async_size) {
		add_pages = get_next_ra_size(ra, max_pages);
		if (ra->size + add_pages <= max_pages) {
			ra->async_size = add_pages;
			ra->size += add_pages;
		} else {
			ra->size = max_pages;
			ra->async_size = max_pages >> 1;
		}
	}

	ra_submit(ra, mapping, filp);
}

@1对应顺序读取命中预读,比如我们当前场景预读了4个page,当读取到index = 1第二个page的时候就会命中该逻辑,会将“预读窗口”向前移动。

满足index == (ra->start + ra->size - ra->async_size)条件,所以向前移动预读窗口:

//start = 4
ra->start += ra->size; 
//根据get_next_ra_size函数,其中cur = ra->size = 4,所以return max =32,直接将预读窗口放大到了最大32
ra->size = get_next_ra_size(ra, max_pages);
//async_size = ra->size
ra->async_size = ra->size;


/*
 *  Get the previous window size, ramp it up, and
 *  return it as the new window size.
 */
static unsigned long get_next_ra_size(struct file_ra_state *ra,
				      unsigned long max)
{
	unsigned long cur = ra->size;

	if (cur < max / 16)
		return 4 * cur;
	if (cur <= max / 2)
		return 2 * cur;
	return max;
}

@2:没有触发1,但是触发异步readahead逻辑,同样继续新的预读逻辑,这里考虑到了多线程同时读一个文件的情况,ra结构体要在多线程之间完成状态切换,可看参考文章《Linux文件预读三》

参考文章:

Linux文件系统预读(一) - 知乎

Linux文件系统预读(二) - 知乎

Linux文件预读(三) - 知乎

深入分析Linux内核File cache机制(上篇) - 知乎

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux 读文件 - readahead预读算法 的相关文章

  • 为 Qt 应用程序创建 Linux 安装

    我刚刚用 Qt Creator 制作了一个很棒的程序 我对自己很满意 如何将其从台式机移至笔记本电脑 那么 最好的方法是安装程序 对吗 对于 Ubuntu 这是一个 Debian 软件包 对吗 我怎么做 有人这样做过吗 他们可以分享 QT
  • 每个进程每个线程的时间量

    我有一个关于 Windows 和 Linux 中进程和线程的时间量子的问题 我知道操作系统通常为每个线程提供固定的时间量 我知道时间量根据前台或后台线程而变化 也可能根据进程的优先级而变化 每个进程有固定的时间量吗 例如 如果操作系统为每个
  • 劫持系统调用

    我正在编写一个内核模块 我需要劫持 包装一些系统调用 我正在暴力破解 sys call table 地址 并使用 cr0 来禁用 启用页面保护 到目前为止一切顺利 一旦完成 我将公开整个代码 因此如果有人愿意 我可以更新这个问题 无论如何
  • 为什么 Linux 对目录使用 getdents() 而不是 read()?

    我浏览 K R C 时注意到 为了读取目录中的条目 他们使用了 while read dp gt fd char dirbuf sizeof dirbuf sizeof dirbuf code Where dirbuf是系统特定的目录结构
  • Composer 安装要求

    我正在尝试将 Composer 安装到 Laravel 项目中 当我做的时候sudo composer install在项目目录中它显示了两个错误 Problem 1 Installation request for simplesoftw
  • 让 MongoDB 在 Linux 上监听远程连接

    我已在 Windows 本地计算机上 上成功安装 MongoDB 作为服务 但现在我想将 MongoDb 移动到单独的服务器 所以我将 tarball 解压到网络上的虚拟服务器 运行 Linux 当我从本地计算机使用 PuTTY 连接到服务
  • 在 Linux 上的 Python 中使用受密码保护的 Excel 工作表

    问题很简单 我每周都会收到一堆受密码保护的 Excel 文件 我必须解析它们并使用 Python 将某些部分写入新文件 我得到了文件的密码 当在 Windows 上完成此操作时 处理起来很简单 我只需导入 win32com 并使用 clie
  • 如何模拟ARM处理器运行环境并加载Linux内核模块?

    我尝试加载我的vmlinux into gdb并使用 ARM 内核模拟器 但我不明白为什么我会得到Undefined target command sim 这是外壳输出 arm eabi gdb vmlinux GNU gdb GDB 7
  • 为什么我收到的数据包数据大小大于mss?

    我在两台 PC 上使用 ifconfig ethX mtu 300 修改了 MTU 并使用 netperf 测试网络 我用 WireShark 嗅探了 SYN 数据包中的 MSS 260 但我得到了一些大于 260 的数据包 为什么 嗅探器
  • 在 Mac OS X 上构建 Linux 内核

    我正在做一个修改Linux内核的项目 我有一台桌面 Linux 机器 在上面构建内核没有问题 不过 我要去旅行 我想在途中工作 我只有一台 MacBook 当我尝试构建 Linux 内核时 它抱怨说elf h was not found 我
  • 在 Linux 上更快地分叉大型进程?

    在现代 Linux 上达到与 Linux 相同效果的最快 最好的方法是什么 fork execve combo 从一个大的过程 我的问题是进程分叉大约 500MByte 大 并且一个简单的基准测试只能从进程中实现约 50 个分叉 秒 比较最
  • 是否可以在Linux上将C转换为asm而不链接libc?

    测试平台为Linux 32位 但也欢迎 Windows 32 位上的某些解决方案 这是一个c代码片段 int a 0 printf d n a 如果我使用 gcc 生成汇编代码 gcc S test c 然后我会得到 movl 0 28 e
  • 创建 jar 文件 - 保留文件权限

    我想知道如何创建一个保留其内容的文件权限的 jar 文件 我将源代码和可执行文件打包在一个 jar 文件中 该文件将在使用前提取 人们应该能够通过运行批处理 shell 脚本文件立即运行示例和演示 然后他们应该能够修改源代码并重新编译所有内
  • Bash 解析和 shell 扩展

    我对 bash 解析输入和执行扩展的方式感到困惑 对于输入来说 hello world 作为 bash 中的参数传递给显示其输入内容的脚本 我不太确定 Bash 如何解析它 Example var hello world displaywh
  • 无法加载 JavaHL 库。- linux/eclipse

    在尝试安装 Subversion 插件时 当 Eclipse 启动时出现此错误 Failed to load JavaHL Library These are the errors that were encountered no libs
  • 强制卸载 NFS 安装目录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Locked 这个问题及其答案是locked help locked posts因为这个问题是题外话 但却具有历史意义 目前不接受新的答案
  • 抑制 makefile 中命令调用的回显?

    我为一个作业编写了一个程序 该程序应该将其输出打印到标准输出 分配规范需要创建一个 Makefile 当调用它时make run gt outputFile应该运行该程序并将输出写入一个文件 该文件的 SHA1 指纹与规范中给出的指纹相同
  • 如何根据 HTTP 请求使用 Python 和 Flask 执行 shell 命令并流输出?

    下列的这个帖子 https stackoverflow com questions 15092961 how to continuously display python output in a webpage 我能够tail f网页的日志
  • Linux TUN/TAP:无法从 TAP 设备读回数据

    问题是关于如何正确配置想要使用 Tun Tap 模块的 Linux 主机 My Goal 利用现有的路由软件 以下为APP1和APP2 但拦截并修改其发送和接收的所有消息 由Mediator完成 我的场景 Ubuntu 10 04 Mach
  • 如何在bash中使用jq从变量中包含的json中提取值

    我正在编写一个 bash 脚本 其中存储了一个 json 值 现在我想使用 Jq 提取该 json 中的值 使用的代码是 json val code lyz1To6ZTWClDHSiaeXyxg redirect to http examp

随机推荐

  • 2020年 IEEE VIS 科学可视化与体渲染论文整理与分析

    因为最近工作的关系 需要研究一下IEEE VIS中2017年以后的与我之前主要方向 体渲染 医学可视化 有关的论文 我把这些年全部的论文进行了筛选和梳理 总共筛选出57篇论文 打算写一个文章来记录这些内容 这个栏目是2020年的5篇论文的介
  • 把Collection转化为XML

    IList
  • zlib库源码编译

    文章目录 zlib库源码编译 简介 源码编译 Windows vs2017 64位版本编译 zlib库源码编译 简介 该篇博客主要用于记录zlib库的源码编译方法 根据自己在源码编译过程中踩得坑进行记录 既便于后续自己学习 也希望能够帮助他
  • SDK 开发中见到的问题

    问题1 Could not build module MySDK 原因是 在pubulic的头文件中导入的头文件都需要导入到pubulic中 m文件中头文件不需要导入 问题2 Missing submodule subHeader 原因是
  • PyDev Eclipse使用技巧说明

    PyDev Package Explorer 创建项目 在开展工作之前 需要创建一个新的项目 在 Eclipse 菜单栏中 选择 File gt New gt Project gt Pydev gt Pydev Project 单击 Nex
  • 求函数【线段树】【2020牛客寒假算法基础集训营2】

    首先 这个区间很容易让人想到是区间操作 那么就是来推公式吧 我们从中不难发现 从两个值到四个值 可以是相当于 其中 两个 括号中的值是不是有点相似 我们是不是可以将它放在线段树上来进行维护了 include
  • CVPR 2022

    作者 cocoon 编辑 3D视觉开发者社区 前言 FAIR又出新作了 一篇 2020年代的卷积网络 的横空出世 让国内外CV圈的眼光都聚焦于此 不少大牛都纷纷下场参与讨论 研究团队以Transformer的一些设计原则以及训练技巧为标 尽
  • 引用与指针有什么区别?

    引用与指针有什么区别 指针和引用都是地址的概念 指针指向一块内存 它的内容是所指内存的地址 引用是某块内存的别名 程序为指针变量分配内存区域 而不为引用分配内存区域 指针使用时要在前加 引用可以直接使用 引用在定义时就被初始化 之后无法改变
  • Java 异常

    目录 1 自己不处理 交给调用者处理 1 1 throws 声明一个异常 1 2 throw 抛出一个异常 2 自己处理异常 2 1 try catch 2 2 try catch的常见问题 3 Throwable 的成员方法 4 自定义异
  • [2021.9.13][OpenGL ES 3.0编程指南]1 OpenGL ES 3.0简介

    1 1 OpenGL ES 3 0 OpenGL ES 3 0实现了具有可编程着色功能的图形管线 由两个规范组成 OpenGL ES 3 0 API规范和OpenGL ES着色语言3 0规范 图1 1展示了OpenGL ES 3 0 图形管
  • java网络编程01——网络基本概念

    阅读 java网络编程 等诸多资料个人所思所想读书笔记 1 网络 因特网 两种方式回答问题 其一是描述因特网的基本构成即构成因特网的基本硬件和软件组件 其二根据分布式应用提供服务的联网基础设施描述因特网 因特网是世界范围的计算机网络 即是一
  • 服务器光信号闪红灯是什么意思,路由器光信号闪红灯是什么意思

    现在不少宽带在安装之后还需要配备一个路由器用来接收光纤信号 在路由器上会有几个指示灯 如果你的路由器信号灯一直闪红灯知道是什么意思吗 一起来了解一下吧 闪红灯的意思 宽带 费 现在宽带基本都是后付费模式 因为 费的时间太 运营商直接关掉了宽
  • 矩阵通高效监管企业新媒体矩阵,账号集中管理与运营数据分析

    越来越多的企业在全网布局旗下账号 希望通过社媒传播矩阵 以内容连接产品与用户 达成增加销售线索或扩大品牌声量的目的 构建矩阵的优势在于 内容能多元发展 聚集不同平台流量 多种营销渠道自主掌控 分散单一平台传播风险 各平台账号间也能协同互补
  • JavaScript中Math.max()和Math.min()方法

    JavaScript中Math max 和Math min 方法 Math是JavaScript中的对象 不是构造函数 可以用来执行数学任务 1 Math max max 返回给定的一组数据中的最大值 但是不接收数组作为参数 参考用法
  • vue3.0 兼容ie浏览器

    vue3 0 兼容ie浏览器 安装babel polyfill npm install save babel polyfill 在main js里面引入 一定要在最上面 第一行 import babel polyfill 安装完成后会有ba
  • HTML页面结构

  • Unity实现点击显示不同UI

    在开发过程中经常遇到切换显示不同UI的需求 实现方案有2套 1 创建两个场景A B 在A中点击某个button后触发切换事件后加载B场景现在新的场景信息 优点 是逻辑简单 在不同的场景中创建对应的UI即可 缺点是当两个场景中有重复显示的模型
  • 高德地图JSAPIvue项目的使用

    最近在项目中使用高德地图JSAPI 遇到一些问题整理一下做个总和记录 希望能帮到看到文章的大家 1 关于引用 npm i amap amap jsapi loader save 然后创建好地图容器后引入地图 注意避坑的点 1 使用loca可
  • vue指令实现埋点

    1 自定义指令 import Vue from vue 自定义埋点指令 Vue directive track 钩子函数 只调用一次 指令第一次绑定到元素时调用 在这里可以进行一次性的初始化设置 el 指令所绑定的元素 可以用来直接操作 D
  • Linux 读文件 - readahead预读算法

    顺序读场景 intmain charc 4096 intin 1 in open news txt O RDONLY intindex 0 while read in c 4096 4096 printf index d len ld n