一文看懂 | 内存交换机制

本文基于 Linux-2.4.16 内核版本

由于计算机的物理内存是有限的, 而进程对内存的使用是不确定的, 所以物理内存总有用完的可能性. 那么当系统的物理内存不足时, Linux内核使用什么方案来避免申请不到物理内存这个问题呢?

相对于内存来说, 磁盘的容量是非常大的, 所以Linux内核实现了一个叫 内存交换 的功能 -- 把某些进程的一些暂时用不到的内存页保存到磁盘中, 然后把物理内存页分配给更紧急的用户使用, 当进程用到时再从磁盘读回到内存中即可. 有了 内存交换 功能, 系统可使用的内存就可以远远大于物理内存的容量.

LRU算法

内存交换 过程首先是找到一个合适的用户进程内存管理结构,然后把进程占用的内存页交换到磁盘中,并断开虚拟内存与物理内存的映射,最后释放进程占用的内存页。由于涉及到IO操作,所以这是一个比较耗时的过程。如果被交换出去的内存页刚好又被访问了,这时又需要从磁盘中把内存页的数据交换到内存中。所以,在这种情况下不单不能解决内存紧缺的问题,而且增加了系统的负荷。

为了解决这个问题,Linux内核使用了一种称为 LRU (Least Recently Used) 的算法, 下面介绍一下 LRU算法 的大体过程.

LRU 的中文翻译是 最近最少使用, 顾名思义就是一段时间内没有被使用, 那么Linux内核怎么知道哪些内存页面最近没有被使用呢? 最简单的方法就是把内存页放进一个队列里, 如果内存页被访问了, 就把内存页移动到链表的头部, 这样没被访问的内存页在一段时间后便会移动到队列的尾部, 而释放内存页时从链表的尾部开始. 著名的缓存服务器 memcached 就是使用这种 LRU算法.

Linux内核也使用了类似的算法, 但相对要复杂一些. Linux内核维护着三个队列: 活跃队列, 非活跃脏队列和非活跃干净队列. 为什么Linux需要维护三个队列, 而不是使用一个队列呢? 这是因为Linux希望内存页交换过程慢慢进行, Linux内核有个内核线程 kswapd 会定时检查系统的空闲内存页是否紧缺, 如果系统的空闲内存页紧缺时时, 就会选择一些用户进程把其占用的内存页添加到活跃链表中并断开进程与此内存页的映射关系. 随着时间的推移, 如果内存页没有被访问, 那么就会被移动到非活跃脏链表. 非活跃脏链表中的内存页是需要被交换到磁盘的, 当系统中空闲内存页紧缺时就会从非活跃脏链表的尾部开始把内存页刷新到磁盘中, 然后移动到非活跃干净链表中, 非活跃干净链表中的内存页是可以立刻分配给进程使用的. 各个链表之间的移动如下图:

lru links

如果在这个过程中, 内存页又被访问了, 那么Linux内核会把内存页移动到活跃链表中, 并且建立内存映射关系, 这样就不需要从磁盘中读取内存页的内容.

注意: 内核只维护着一个活跃链表和一个非活跃脏链表, 但是非活跃干净链表是每个内存管理区都有一个的. 
这是因为分配内存是在内存管理区的基础上进行的, 所以一个内存页必须属于某一个内存管理区.

kswapd内核线程

在Linux系统启动时会调用 kswapd_init() 函数, 代码如下:

static int __init kswapd_init(void)
{printk("Starting kswapd v1.8\n");swap_setup();kernel_thread(kswapd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);kernel_thread(kreclaimd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);return 0;
}

可以看到, kswapd_init() 函数会创建 kswapd 和 kreclaimd 两个内核线程, 这两个内核线程负责在系统物理内存紧缺时释放一些物理内存页, 从而使系统的可用内存达到一个平衡. 下面我们重点来分析 kswapd 这个内核线程, kswapd() 的源码如下:

int kswapd(void *unused)
{struct task_struct *tsk = current;tsk->session = 1;tsk->pgrp = 1;strcpy(tsk->comm, "kswapd");sigfillset(&tsk->blocked);kswapd_task = tsk;tsk->flags |= PF_MEMALLOC;for (;;) {static int recalc = 0;if (inactive_shortage() || free_shortage()) {int wait = 0;/* Do we need to do some synchronous flushing? */if (waitqueue_active(&kswapd_done))wait = 1;do_try_to_free_pages(GFP_KSWAPD, wait);}refill_inactive_scan(6, 0);if (time_after(jiffies, recalc + HZ)) {recalc = jiffies;recalculate_vm_stats();}wake_up_all(&kswapd_done);run_task_queue(&tq_disk);if (!free_shortage() || !inactive_shortage()) {interruptible_sleep_on_timeout(&kswapd_wait, HZ);} else if (out_of_memory()) {oom_kill();}}
}

kswapd 内核线程由一个无限循环组成, 首先通过 inactive_shortage() 和 free_shortage() 函数判断系统的非活跃页面和空闲物理内存页是否短缺, 如果短缺的话, 那么就调用 do_try_to_free_pages() 函数试图释放一些物理内存页. 然后通过调用 refill_inactive_scan() 函数把一些活跃链表中的内存页移动到非活跃脏链表中. 最后, 如果空闲物理内存页或者非活跃内存页不短缺, 那么就让 kswapd 内核线程休眠一秒.

接下来我们分析一下 do_try_to_free_pages() 函数做了一些什么工作, 代码如下:

static int do_try_to_free_pages(unsigned int gfp_mask, int user)
{int ret = 0;if (free_shortage() || nr_inactive_dirty_pages > nr_free_pages() + nr_inactive_clean_pages())ret += page_launder(gfp_mask, user);if (free_shortage() || inactive_shortage()) {shrink_dcache_memory(6, gfp_mask);shrink_icache_memory(6, gfp_mask);ret += refill_inactive(gfp_mask, user);} else {kmem_cache_reap(gfp_mask);ret = 1;}return ret;
}

do_try_to_free_pages() 函数第一步先判断系统中的空闲物理内存页是否短缺, 或者非活跃脏页面的数量大于空闲物理内存页和非活跃干净页面的总和, 其中一个条件满足了, 就调用 page_launder() 函数把非活跃脏链表中的页面刷到磁盘中, 然后移动到非活跃干净链表中. 接下来如果内存还是紧缺的话, 那么就调用 shrink_dcache_memory()shrink_icache_memory() 和 refill_inactive() 函数继续释放内存.

下面我们先来分析一下 page_launder() 这个函数, 由于这个函数很长, 所以我们分段来解释:

int page_launder(int gfp_mask, int sync)
{int launder_loop, maxscan, cleaned_pages, maxlaunder;int can_get_io_locks;struct list_head * page_lru;struct page * page;can_get_io_locks = gfp_mask & __GFP_IO; // 是否需要进行写盘操作launder_loop = 0;maxlaunder = 0;cleaned_pages = 0;dirty_page_rescan:spin_lock(&pagemap_lru_lock);maxscan = nr_inactive_dirty_pages;// 从非活跃脏链表的后面开始扫描while ((page_lru = inactive_dirty_list.prev) != &inactive_dirty_list &&maxscan-- > 0) {page = list_entry(page_lru, struct page, lru);...

上面的代码首先把 pagemap_lru_lock 上锁, 然后从尾部开始遍历非活跃脏链表.

        // 如果满足以下的任意一个条件, 都表示内存页在使用中, 把他移动到活跃链表if (PageTestandClearReferenced(page) ||             // 如果设置了 PG_referenced 标志page->age > 0 ||                            // 如果age大于0, 表示页面被访问过(!page->buffers && page_count(page) > 1) || // 如果页面被其他进程映射page_ramdisk(page)) {                       // 如果用于内存磁盘的页面del_page_from_inactive_dirty_list(page);add_page_to_active_list(page);continue;}

上面代码判断内存页是否能需要重新移动到活跃链表中, 依据有:

  • 内存页是否设置了 PG_referenced 标志;

  • 内存页的age字段是否大于0 (age字段是内存页的生命周期);

  • 内存页是否还有映射关系;

  • 内存页是否用于内存磁盘.

如果满足上面其中一个条件, 都需要重新把内存页移动到活跃页面中.

        if (PageDirty(page)) { // 如果页面是脏的, 那么应该把页面写到磁盘中int (*writepage)(struct page *) = page->mapping->a_ops->writepage;int result;if (!writepage)goto page_active;/* First time through? Move it to the back of the list */if (!launder_loop) { // 第一次只把页面移动到链表的头部, 这是为了先处理已经干净的页面list_del(page_lru);list_add(page_lru, &inactive_dirty_list);UnlockPage(page);continue;}/* OK, do a physical asynchronous write to swap.  */ClearPageDirty(page);page_cache_get(page);spin_unlock(&pagemap_lru_lock);result = writepage(page);page_cache_release(page);/* And re-start the thing.. */spin_lock(&pagemap_lru_lock);if (result != 1)continue;/* writepage refused to do anything */set_page_dirty(page);goto page_active;}

上面的代码首先判断内存页是否脏的(是否设置了 PG_dirty 标志), 如果是, 那么就需要把内存页刷新到磁盘中. 这里有个要主要的地方是, 当 launder_loop 变量为0时只是把内存页移动到非活跃脏链表的头部. 当 launder_loop 变量为1时才会把内存页刷新到磁盘中. 为什么要这样做呢? 这是因为Linux内核希望第一次扫描先把非活跃脏链表中的干净内存页移动到非活跃干净链表中, 第二次扫描才把脏的内存页刷新到磁盘中. 后面的代码会对 launder_loop 变量进行修改. 而且我们发现, 把脏页面刷新到磁盘后, 并没有立刻把内存页移动到非活跃干净链表中, 而是简单的清除了 PG_dirty 标志.

        if (page->buffers) { // 涉及文件系统部分, 先略过...} else if (page->mapping && !PageDirty(page)) { // 内存页是干净的, 移动到非活跃干净链表del_page_from_inactive_dirty_list(page);add_page_to_inactive_clean_list(page);UnlockPage(page);cleaned_pages++;} else {
page_active:del_page_from_inactive_dirty_list(page);add_page_to_active_list(page);UnlockPage(page);}

上面的代码比较简单, 如果内存页已经是干净的, 那么久移动到非活跃干净链表中.

    if (can_get_io_locks && !launder_loop && free_shortage()) {launder_loop = 1;/* If we cleaned pages, never do synchronous IO. */if (cleaned_pages)sync = 0;/* We only do a few "out of order" flushes. */maxlaunder = MAX_LAUNDER;/* Kflushd takes care of the rest. */wakeup_bdflush(0);goto dirty_page_rescan;}/* Return the number of pages moved to the inactive_clean list. */return cleaned_pages;
}

从上面的代码可以看到, 当 can_get_io_locks 等于1(gfp_mask 设置了 __GFP_IO 标志), launder_loop 等于0, 并且空闲内存页还是短缺(free_shortage() 为真)的情况下, 把 launder_loop 变量被设置为1, 并且跳转到 dirty_page_rescan 处重新扫描, 这是第二次扫描非活跃脏链表, 会把脏的内存页刷新到磁盘中.

接下来我们继续分析 refill_inactive() 这个函数:

static int refill_inactive(unsigned int gfp_mask, int user)
{int priority, count, start_count, made_progress;count = inactive_shortage() + free_shortage();if (user)count = (1 << page_cluster);start_count = count;...priority = 6;do {made_progress = 0;if (current->need_resched) {__set_current_state(TASK_RUNNING);schedule();}while (refill_inactive_scan(priority, 1)) { // 把活跃页面链表中的页面移动到非活跃脏页面链表中made_progress = 1;if (--count <= 0)goto done;}...while (swap_out(priority, gfp_mask)) { // 把一些用户进程映射的内存页放置到活跃页面链表中made_progress = 1;if (--count <= 0)goto done;}if (!inactive_shortage() || !free_shortage())goto done;if (!made_progress)priority--;} while (priority >= 0);while (refill_inactive_scan(0, 1)) {if (--count <= 0)goto done;}done:return (count < start_count);
}

在这个函数中, 我们主要关注两个地方:

  • 调用 refill_inactive_scan() 函数, refill_inactive_scan() 函数的作用是把活跃链表中的内存页移动到非活跃脏链表中.

  • 调用 swap_out() 函数, swap_out() 函数的作用是选择一个用户进程, 并且把其映射的内存页添加到活跃链表中.

先来看看 refill_inactive_scan() 函数:

int refill_inactive_scan(unsigned int priority, int oneshot)
{struct list_head * page_lru;struct page * page;int maxscan, page_active = 0;int ret = 0;spin_lock(&pagemap_lru_lock);maxscan = nr_active_pages >> priority;while (maxscan-- > 0 && (page_lru = active_list.prev) != &active_list) {page = list_entry(page_lru, struct page, lru);.../* Do aging on the pages. */if (PageTestandClearReferenced(page)) {age_page_up_nolock(page);page_active = 1;} else {age_page_down_ageonly(page); // page->age = page->age / 2if (page->age == 0 && page_count(page) <= (page->buffers ? 2 : 1)) {deactivate_page_nolock(page); // 把页面放置到非活跃脏页面链表page_active = 0;} else {page_active = 1;}}if (page_active || PageActive(page)) {list_del(page_lru);list_add(page_lru, &active_list);} else {ret = 1;if (oneshot)break;}}spin_unlock(&pagemap_lru_lock);return ret;
}

refill_inactive_scan() 函数比较简单, 首先从活跃链表的尾部开始遍历, 然后判断内存页的生命是否已经用完(age是否等于0), 并且没有进程与其有映射关系(count是否等于1). 如果是, 那么就调用 deactivate_page_nolock() 函数把内存页移动到非活跃脏链表中.

接着来看看 swap_out() 函数, swap_out() 函数比较复杂, 但最终会调用 try_to_swap_out() 函数, 所以我们只分析 try_to_swap_out() 函数:

static int try_to_swap_out(struct mm_struct * mm, struct vm_area_struct* vma, unsigned long address, pte_t * page_table, int gfp_mask)
{...page = pte_page(pte);if (!mm->swap_cnt)return 1;mm->swap_cnt--;...if (PageSwapCache(page)) { // 内存页之前已经发生过交换操作entry.val = page->index;if (pte_dirty(pte))set_page_dirty(page);
set_swap_pte:swap_duplicate(entry);// 把页目录项设置为磁盘交换区的信息(注意:此时是否在内存中标志位为0, 所以访问这个内存地址会触发内存访问异常)set_pte(page_table, swp_entry_to_pte(entry));
drop_pte:UnlockPage(page);mm->rss--;deactivate_page(page);page_cache_release(page);
out_failed:return 0;}...entry = get_swap_page();if (!entry.val)goto out_unlock_restore; /* No swap space left */add_to_swap_cache(page, entry);set_page_dirty(page);goto set_swap_pte;out_unlock_restore:set_pte(page_table, pte);UnlockPage(page);return 0;
}

上面的代码中, 首先调用 get_swap_page() 函数获取交换文件的一个槽(用于保存内存页的内容), 然后调用 add_to_swap_cache() 函数把内存页添加到活跃链表中, add_to_swap_cache() 函数源码如下:

void add_to_swap_cache(struct page *page, swp_entry_t entry)
{...add_to_page_cache_locked(page, &swapper_space, entry.val);
}void add_to_page_cache_locked(struct page * page, struct address_space *mapping, unsigned long index)
{if (!PageLocked(page))BUG();page_cache_get(page);spin_lock(&pagecache_lock);page->index = index;add_page_to_inode_queue(mapping, page);add_page_to_hash_queue(page, page_hash(mapping, index));lru_cache_add(page);spin_unlock(&pagecache_lock);
}

add_to_swap_cache() 函数会调用 add_to_page_cache_locked() 函数, 而add_to_page_cache_locked() 函数会调用 lru_cache_add() 函数来把内存页添加到活跃链表中, lru_cache_add() 函数代码如下:

#define add_page_to_active_list(page) {     \DEBUG_ADD_PAGE                          \ZERO_PAGE_BUG                           \SetPageActive(page);                    \list_add(&(page)->lru, &active_list);   \nr_active_pages++;                      \
}void lru_cache_add(struct page * page)
{spin_lock(&pagemap_lru_lock);if (!PageLocked(page))BUG();DEBUG_ADD_PAGEadd_page_to_active_list(page);/* This should be relatively rare */if (!page->age)deactivate_page_nolock(page);spin_unlock(&pagemap_lru_lock);
}

从上面的代码可以看到, lru_cache_add() 函数最终会调用 list_add(&(page)->lru, &active_list) 这行代码来把内存页添加到活跃链表(active_list)中, 并设置内存页的 PG_active 标志.

最后我们通过一幅图来总结一下 kswapd 内核线程的流程:

kswap()
└→ do_try_free_pages()└→ page_launder()└→ refill_inactive()└→ refill_inactive_scan()└→ swap_out()

swap_out() 函数会把进程占用的内存页添加到活跃链表中, 而 refill_inactive_scan() 函数会把活跃链表的内存页移动到非活跃脏链表中, 最后 page_launder() 会把非活跃脏链表的内存页刷新到磁盘并且移动到非活跃干净链表中, 非活跃干净链表中的内存页是直接可以用来分配使用的.


推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

嵌入式Linux

微信扫描二维码,关注我的公众号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465404.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无论是cisco还是华三的书上对于子网划分有个问题需要解释

无论是cisco还是华三的书上对于子网划分有个问题&#xff0c;例如&#xff1a;如果子网为有五位 &#xff0c;则可以划分为30个子网。在实际中却不是这样的 子网位五位&#xff0c;可以划分为32个子网。那为什么这么写&#xff0c;难道是出书的人写错了&#xff0c;其实不是。这…

mysql online ddl 5.6_MySQL 5.6的Online DDL功能测试

online DDL的前身是 innodb fast index creation(5.1和5.5), 5.6里对这个功能做了扩展&#xff1a;很多alter table的操作绕开了 table copying&#xff0c;支持DML并发操作。一、online ddl的支持测试&#xff1a;1、主键的增删主键添加&#xff1a;支持online ddl&#xff0c…

Stupid cat Doge (分形图)

【题目描述】 【题目链接】 http://noi.openjudge.cn/ch0204/8463/ 【算法】 为求等级N下的点的坐标可由几何关系找到其与等级N-1下对应点的关系&#xff0c;然后递归直至所有点的祖先&#xff08;等级0&#xff09;即可计算出坐标。 【代码】 1 #include <bits/stdc.h>2…

电赛时,如何快速搭建电路?

大家好&#xff0c;我是张巧龙&#xff0c;电赛只有四天三夜&#xff0c;电路方案可能需要多次验证&#xff0c;有的同学选择直接洞洞板焊接&#xff0c;自行跳线。有些同学可能会选择雕刻机雕刻。我带的学生一般会使用传统工艺-腐蚀法&#xff0c;这种方法的优点&#xff0c;成…

Javaweb经典三层架构的演变

1.Javaweb经历了三个时期 ①JSP Model1第一代 JSP Model1是JavaWeb早期的模型&#xff0c;它适合小型Web项目&#xff0c;开发成本低&#xff01;Model1第一代时期&#xff0c;服务器端只有JSP页面&#xff0c;所有的操作都在JSP页面中&#xff0c;连访问数据库的API也在JSP页面…

我决定去读研了

读书这个事情&#xff0c;我一直都是比较推荐大家去做的&#xff0c;今天有一位同学找到我&#xff0c;问我现在哪个行业比较赚钱&#xff0c;自己马上要毕业了&#xff0c;想马上大显身手一下。然后我问他&#xff0c;你有没有赚钱的压力&#xff0c;还有自己目前的学习成绩怎…

win2008 php mysql zend phpmyadmin_Windows2008 最新版Apache2.PHP5.MySQL6.PHPMyadmin.ZendOptimizer安装图解...

首先到PHPCHINA的网站www.phpchina.com下载最新的正式版本Apache 2.2.8地址&#xff1a;http://apache.mirror.phpchina.com/httpd/httpd-2.2.8-win32-src.zipPHP-5.2.5地址&#xff1a;http://cn.php.net/get/php-5.2.5-Win32.zip/from/this/mirrorMySQL-6.0.3-alpha-win32地址…

WPF 开源项目 【watcher】 守望者,一款监控,统计,分析你每天在自己电脑上究竟干了什么的软件...

时隔多年&#xff08;两年&#xff09;&#xff0c;天天沉迷写PHP的我在连续加薪了逐渐发现自己不怎么写代码了。 甚至有一天我发现我连IDE 都没有打开&#xff0c;实在是太堕落了 为了及时悬崖勒马&#xff0c;回头是岸。为了鼓励自己专心写代码&#xff0c;我决定写一款监控自…

Java iText PDF:用 iText 包生成简单的 pdf 文件

有点兴趣想要看看 Java 怎么生成 PDF 文件&#xff0c;搜索了一下&#xff0c;据说 iText 包比较好&#xff0c;到 这里下载 iText.jar 包&#xff0c;顺便把源代码、文档都一起下载了吧。但是&#xff0c;仅仅有这么一些材料想要写代码生成 pdf 文件还是比较困难的&#xff0c…

java互换_两个变量交换的四种方法(Java)

对于两种变量的交换,我发现四种方法,下面我用Java来演示一下。 1.利用第三个变量交换数值,简单的方法。 (代码演示一下) 1 class TestEV 2 //创建一个类 3 {4 public static void main(String[]args) 5 {6 int x =5,y=10; //定义两个变量 7 8 int temp = x;    //定义第…

搞技术的,越老越吃香?越老越值钱?

在我的整个职业生涯中&#xff0c;我目睹了很多才华横溢的工程师经过一段不太愉快的短期任职后离开了公司。曾经我也有这样的经历&#xff1a;在一家公司任职不到两年&#xff0c;就跳槽到了另一家&#xff0c;担任工程副总裁。撇开我的例子不谈&#xff0c;我认为出现这种现象…

ubuntu 修改host,以便在本地调试

sudo gedit /etc/hosts这样已经可以即可生效了&#xff0c;另外再改一下nginx.confserver_name localhost www.15-1688.com;

对存储还搞不清楚,先看看这篇文章-从51单片机上去理解存储器

存储器是许多存储单元的集合&#xff0c;存储器单元实际上是时序逻辑电路&#xff08;锁存器&#xff09;的一种&#xff0c;按单元号顺序排列。每个单元由若干二进制位构成&#xff0c;以表示存储单元中存放的数值&#xff0c;这种结构和数组的结构非常相似。按存储器的使用类…

继续泼冷水 Ubuntu决定放弃支持安腾

在今年早些时候微软宣布下一代windows server操作系统不再支持英特尔安腾架构&#xff0c;给本就前途堪忧的安腾泼了一盆冷水。如今Ubuntu&#xff08;重要的一款以桌面应用为主的Linux操作系统&#xff09;也决定不再支持英特尔安腾架构。而另一方面&#xff0c;安腾在硬件厂商…

给不起钱的大公司,算不上大公司

这是昨天给一位同学聊职业规划中说的一句话。这位同学在每次做选择前都会跟我聊下&#xff0c;感觉职业规划咨询跟卖菜差不多&#xff0c;那些觉得我的建议不错的同学&#xff0c;总是会在一年两年后继续找我&#xff0c;挺好的&#xff0c;喜欢跟大家一起沟通吹水并一起成长的…

粉丝回馈,8000元大礼包免费相送

9月开学季&#xff0c;福利送不停&#xff0c;果哥又给大家送福利了&#xff0c;价值8000元豪礼相送&#xff01;一、 前言非常感谢大家对我公众号的支持&#xff0c;暑假也结束了。这次邀请了3个不错的公众号朋友&#xff0c;同时也是电子嵌入式领域的优质公众号&#xff0c;一…

[Drupal] How to get the real path of a node, no matter it is a path or a url alias

Just test the code as below: 代码 $urisubstr(request_uri(),1,strlen(request_uri()));$uri_arrayarray();if(!($pathdrupal_lookup_path(source,$uri))) {$uri_arrayexplode(/,$uri); }else{$uri_arrayexplode(/,$path); }var_dump($uri_array);Have fun!

图文讲解5G调制,特别通俗易懂!

大家好&#xff0c;今天我们来聊聊调制。说到调制&#xff0c;我想很多同学马上会联想到这些关键词&#xff1a;BPSK、QPSK、调幅、调相、QAM、星座图……众所周知&#xff0c;调制和解调是通信基本业务流程中的重要组成部分。没有它们&#xff0c;我们的移动通信根本无法实现。…

2018 Multi-University Training Contest 3

好像克拉丽丝小姐姐题解写的超详细我都没啥好说的了 Problem A. Ascending Rating 仔细一看m是固定的单调DQ就好了 1 #include <bits/stdc.h>2 using namespace std;3 typedef long long LL;4 const int maxn 1e7 10;5 int st, ed, deq[maxn];6 int a[maxn];7 8 inline…

内核抢占,让世界变得更美好 | Linux 内核

大家好&#xff0c;我是老吴。今天要分享的是抢占相关的基础知识。本文以内核抢占为引子&#xff0c;概述一下 Linux 抢占的图景。我尽量避开细节问题和源码分析。什么是内核抢占&#xff1f;别急&#xff0c;咱们慢慢来。先理解抢占 (preemption) 这个概念&#xff1a;involun…