2024年8月

写在前面

本随笔是非常菜的菜鸡写的。如有问题请及时提出。

可以联系:1160712160@qq.com

GitHhub:
https://github.com/WindDevil
(目前啥也没有

本节重点

本章最开始的时候讲解了有类似于多道程序与协作式调度的区别.

回想上一节,我们提到的,如果我们仍然是不使用上一节实现的
yeild
,仍然和上上节(多道程序加载)的实现效果是一样的.

因为如果我们不主动释放CPU,任务仍然是顺序执行的.

那么并不是所有的程序员都会在写程序的时候考虑到别人.比如写单片机的代码,我使用
IIC
通信,我就嗯等
ACK
信号,我宁愿
while()
也不愿意放弃CPU给你.

这时候使用抢占式调度就非常有必要了.

官方文档
有云:我们需要对
任务
的概念进行进一步扩展和延伸,

  • 分时多任务:操作系统管理每个应用程序,以时间片为单位来分时占用处理器运行应用。
  • 时间片轮转调度:操作系统在一个程序用完其时间片后,就抢占当前程序并调用下一个程序执行,周而复始,形成对应用程序在任务级别上的时间片轮转调度。

文档中还有一些概念:

  1. 而随着技术的发展,涌现了越来越多的
    交互式应用
    (Interactive Application) ,它们要达成的一个重要目标就是提高用户(应用的使用者和开发者)操作的响应速度,减少
    延迟
    (Latency),这样才能优化应用的使用体验和开发体验。对于这些应用而言,即使需要等待外设或某些事件,它们也不会倾向于主动 yield 交出 CPU 使用权,因为这样可能会带来无法接受的延迟。
  2. 抢占式调度
    (Preemptive Scheduling) 则是应用
    随时
    都有被内核切换出去的可能。

现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。一般将
时间片
(Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。

调度算法需要考虑:每次在换出之前给一个应用多少时间片去执行,以及要换入哪个应用。可以从性能(主要是吞吐量和延迟两个指标)和
公平性
(Fairness) 两个维度来评价调度算法,后者要求多个应用分到的时间片占比不应差距过大。

这里插一句,这时候往往就想到写
RT-Thread
的时候给每个应用添加一个
500ms
时的纠结.(当然现在想到
RT-Thread
似乎应该有更先进的调度算法,我心里悬着的石头就放下了).

这时候就提到
调度算法
的重要性了(怪不得这个专门有一个岗位).

时间片轮转调度

在这个项目中,使用的调度算法就是
时间片轮转算法
(RR, Round-Robin) .
本章中我们仅需要最原始的 RR 算法,用文字描述的话就是维护一个任务队列,每次从队头取出一个应用执行一个时间片,然后把它丢到队尾,再继续从队头取出一个应用,以此类推直到所有的应用执行完毕。

时间片的实现

时间片我们在本章开始的时候猜想它是用定时器加中断实现的,目前看来也确实如此.

risc-v的中断

官方文档
对中断的解析是这样的,很深刻,尤其是提到了同步和异步的概念:
在 RISC-V 架构语境下,
中断
(Interrupt) 和我们第二章中介绍的异常(包括程序错误导致或执行 Trap 类指令如用于系统调用的
ecall
)一样都是一种 Trap ,但是它们被触发的原因却是不同的。对于某个处理器核而言, 异常与当前 CPU 的指令执行是
同步
(Synchronous) 的,异常被触发的原因一定能够追溯到某条指令的执行;而中断则
异步
(Asynchronous) 于当前正在进行的指令,也就是说中断来自于哪个外设以及中断如何触发完全与处理器正在执行的当前指令无关。

关于中断,这里提到一个关键点,
检查中断是在每次处理器执行完指令之后
的:
对于中断,可以理解为发起中断的是一套与处理器执行指令无关的电路(从时钟中断来看就是简单的计数和比较器),这套电路仅通过一根导线接入处理器。当外设想要触发中断的时候则输入一个高电平或正边沿,处理器会在每执行完一条指令之后检查一下这根线,看情况决定是继续执行接下来的指令还是进入中断处理流程。也就是说,大多数情况下,指令执行的相关硬件单元和可能发起中断的电路是完全独立
并行
(Parallel) 运行的,它们中间只有一根导线相连。

有关于RISC-V的中断类型和优先级规则,还是要仔细阅读
官方文档
.

这里只提出其中比较重点的一个规则,就是进入中断之后,会
屏蔽低于和等于该优先级的其他中断
.

时钟中断

这里提到了
RISC-V架构
的计时机制,要求它一定要有一个内置时钟.

这里就让我们想起了之前学到过的
Cortex-M3
内置的
systick
.还有很少人注意到的内置在
Cortex-M3
中的
DWT
模块.

(这里也可以去做
第一章作业

第三题
那个延时程序了,有没有记得我们之前去访问
sepc
寄存器的时候使用的
riscv::register
,现在我们可以直接调用它来实现了)


RISC-V 64
架构中,这个计时器的具体实现是
mtime
寄存器.

这个寄存器的不能在
S
特权级直接访问,回想我们之前学过的
OS
架构层次:

创建
os/src/timer.rs
文件.

只需要在
M
特权级的上层(
SBI
)构建一个接口就行了.
RustSBI
恰恰帮我们实现了这样的一个接口,因此我们可以这样实现:

// os/src/timer.rs

use riscv::register::time;

pub fn get_time() -> usize {
    time::read()
}

为了实现
定时一段时间
强行切换应用的效果,我们需要实现一个
定时中断
的特性.

RustSBI
可以实现这个功能,在
os/src/sbi.rs
里使用
sbi_call
实现这个功能,即设置一个计时器,实际原理是设置
mtimecmp
模块的值,等待
mtime
计时达到之后
触发中断
:

// os/src/sbi.rs

pub fn set_timer(time: usize)
{
    sbi_rt::set_timer(time as _);
}

这里
注意
,
手册中写的代码

旧版
,在按照第0章配置的基础上
sbi-rt
的版本是
0.02
.应该使用
如上
代码.

这时候在
os/src/timer.rs
里进行封装.计算计时器在
10ms
内的增量,然后把当前
mtime
的值和增量加起来设置到
mtimecmp
中:

// os/src/timer.rs

use crate::config::CLOCK_FREQ;
const TICKS_PER_SEC:usize  = 100;

pub fn set_next_trigger()
{
	set_timer(get_time()+CLOCK_FREQ/TICKS_PER_SEC);
}

这里注意
CLOCK_FREQ
是写在
os/src/config.rs
里的常量,这里直接参考
源码
即可,这里可以看到
K210
的时钟频率和
QEMU
默认的时钟频率:

/*
#[cfg(feature = "board_k210")]
pub const CLOCK_FREQ: usize = 403000000 / 62;

#[cfg(feature = "board_qemu")]
pub const CLOCK_FREQ: usize = 12500000;
*/
pub use crate::board::CLOCK_FREQ;

这个
board
模块需要在
src/board/qemu.rs
里实现.

//! Constants used in rCore for qemu

pub const CLOCK_FREQ: usize = 12500000;

最后
board
模块和
timer
模块也需在
os/src/main.rs
里声明.

...
mod timer;

#[path = "boards/qemu.rs"]
mod board;
...

#[path = "boards/qemu.rs"]
是一个属性宏 (
attribute macro
),用来告诉 Rust 编译器该模块的实际源文件位置是在
boards/qemu.rs
这个文件中.

还需要实现一个
获取当前定时器时间
的函数.可以
用来
统计应用运行时长.

// os/src/timer.rs

const MICRO_PER_SEC: usize = 1_000_000;

pub fn get_time_us() -> usize {
    time::read() / (CLOCK_FREQ / MICRO_PER_SEC)
}

这里官方文档说要实现一个
系统调用
,用于应用获取当前时间.那么我们已经有了
get_time_us
函数了.

根据上一节的经验,我们只需要分别在
用户层

内核层
写实现即可.

首先是在用户层实现:

// user/src/syscall.rs

const SYSCALL_GET_TIME: usize = 169;

pub fn sys_get_time() -> isize {
    syscall(SYSCALL_GET_TIME, [0, 0, 0])
}

进一步封装:

// user/src/lib.rs

pub fn get_time() -> isize {
    sys_get_time()
}

然后需要在内核层实现,这里是回调:

// os/src/syscall/mod.rs

...
const SYSCALL_GET_TIME: usize = 169;

/// handle syscall exception with `syscall_id` and other arguments
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
    match syscall_id {
        SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
        SYSCALL_EXIT => sys_exit(args[0] as i32),
        SYSCALL_YIELD => sys_yield(),
        SYSCALL_GET_TIME => sys_get_time(),
        _ => panic!("Unsupported syscall_id: {}", syscall_id),
    }
}
...

这里还需要具体实现
sys_get_time
:

// os/src/syscall/process.rs

use crate::timer::get_time_us;

pub fn sys_get_time() -> isize {
    get_time_us() as isize
}

那么我们也不能直接返回当前
mtime
的值,而是需要更换单位,因此
get_time_us
的实现也显而易见,根据
系统时钟频率
计算出当前的
微秒级
时间:

// os/src/timer.rs

const MICRO_PER_SEC: usize = 1_000_000;
pub fn get_time_us() -> usize
{
    time::read()/(CLOCK_FREQ/MICRO_PER_SEC)
}

抢占式调度

这里要回忆起
中断也是一种
trap

.

那么要实现抢占式调度就很简单了.只需要在发生
定时器中断
之后
继续设置一个定时器
,然后执行
suspend_current_and_run_next
,
挂起当前应用并且执行下一个应用
.

那么只需要修改
trap_handler
函数,加入相应的处理逻辑.

// os/src/trap/mod.rs

match scause.cause() {
    Trap::Interrupt(Interrupt::SupervisorTimer) => {
        set_next_trigger();
        suspend_current_and_run_next();
    }
}

那么第一个时间片是哪里来的呢,答案是我们
自己设置一个定时器
timer::set_next_trigger();
.为了避免
S
特权级时钟中断被屏蔽,我们需要
启动时钟中断的陷入
trap::enable_timer_interrupt();
.

// os/src/main.rs

#[no_mangle]
pub fn rust_main() -> ! {
    clear_bss();
    println!("[kernel] Hello, world!");
    trap::init();
    loader::load_apps();
    trap::enable_timer_interrupt();
    timer::set_next_trigger()
    task::run_first_task();
    panic!("Unreachable in rust_main!");
}

trap::enable_timer_interrupt();
实际是设置了
sie
寄存器的
stie
位,使得
S
特权级时钟中断不会被屏蔽.

// os/src/trap/mod.rs

/// timer interrupt enabled
pub fn enable_timer_interrupt() {
    unsafe {
        sie::set_stimer();
    }
}

这里这个特殊的处理机制也需要关注,虽然以我的水平暂时发现不了这么高深的玩意:
有同学可能会注意到,我们并没有将应用初始 Trap 上下文中的
sstatus
中的
SPIE
位置为 1 。这将意味着 CPU 在用户态执行应用的时候
sstatus

SIE
为 0 ,根据定义来说,此时的 CPU 会屏蔽 S 态所有中断,自然也包括 S 特权级时钟中断。但是可以观察到我们的应用在用尽一个时间片之后能够正常被打断。这是因为当 CPU 在 U 态接收到一个 S 态时钟中断时会被抢占,这时无论
SIE
位是否被设置都会进入 Trap 处理流程。

这时候要注意
主动交出CPU
的机制仍然需要保留.比如这个应用:

// user/src/bin/03sleep.rs

#[no_mangle]
fn main() -> i32 {
    let current_timer = get_time();
    let wait_for = current_timer + 3000;
    while get_time() < wait_for {
        yield_();
    }
    println!("Test sleep OK!");
    0
}

这里如果不执行
yield_()
就需要
等定时器中断
需要
10ms
的时间片,这样就
浪费了时间
.

具体实现执行

这里主要是对
分时多任务系统与抢占式调度
的测试应用没有实现.

我们只需在
源码
中找到具体实现就行了.

这里是一些解析和提点:

// user\src\bin\00power_3.rs

#![no_std]
#![no_main]

#[macro_use]
extern crate user_lib;

const LEN: usize = 100;

#[no_mangle]
fn main() -> i32 {
    let p = 3u64;
    let m = 998244353u64;
    let iter: usize = 200000;
    let mut s = [0u64; LEN];
    let mut cur = 0usize;
    s[cur] = 1;
    for i in 1..=iter {
        let next = if cur + 1 == LEN { 0 } else { cur + 1 };
        s[next] = s[cur] * p % m;
        cur = next;
        if i % 10000 == 0 {
            println!("power_3 [{}/{}]", i, iter);
        }
    }
    println!("{}^{} = {}(MOD {})", p, iter, s[cur], m);
    println!("Test power_3 OK!");
    0
}

这个应用计算
\(p^{iter} \mod m\)
的值:

  1. 计算当前元素
    s[cur]
    乘以
    p
    并对
    m
    取模的结果,然后将这个结果存放在下一个位置
    next
    中。
  2. 更新
    cur

    next
    ,如果
    cur
    已经到达数组的末尾 (
    LEN
    ),则将
    cur
    设置为 0,实现数组的滚动使用。
  3. 每当迭代达到 10000 的倍数时,输出当前迭代的进度。
  4. 在完成所有的迭代后,输出最终的计算结果和测试通过的信息。

其余的两个app,
user\src\bin\01power_5.rs

user\src\bin\02power_7.rs
,只是更换了
p
的值.这里掠过.

对于最后一个应用就是我们上一节最后提到的应用
user\src\bin\03sleep.rs
.也不进行赘述.

这里具体把app都实现之后:

cd user
make build
cd ../os
make run

最后得出运行结果:

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] trap init end
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Test sleep OK!
[kernel] Application exited with code 0
All applications completed!

可以看到在
sleep
期间成功运行了三种求幂任务.

二叉搜索树算法实现原理

二叉搜索树(Binary Search Tree,简称BST)是一种节点有序排列的二叉树数据结构。它具有以下性质:

  • 每个节点最多有两个子节点。
  • 对于每个节点,其左子树的所有节点值都小于该节点值,其右子树的所有节点值都大于该节点值。

实现基本步骤和代码示例

步骤

  • 定义节点类:包含节点值、左子节点和右子节点。
  • 插入节点:递归或迭代地将新值插入到树中合适的位置。
  • 搜索节点:根据节点值在树中查找特定值。
  • 删除节点:从树中删除特定值的节点,并维护树的结构。
  • 遍历树:包括前序遍历、中序遍历、后序遍历和层次遍历等。

完整代码示例

namespace HelloDotNetGuide.常见算法
{
    public class 二叉搜索树算法
    {
        public static void BinarySearchTreeRun()
        {
            var bst = new BinarySearchTree();

            // 插入一些值到树中
            bst.Insert(50);
            bst.Insert(30);
            bst.Insert(20);
            bst.Insert(40);
            bst.Insert(70);
            bst.Insert(60);
            bst.Insert(80);
            bst.Insert(750);

            Console.WriteLine("中序遍历(打印有序数组):");
            bst.InorderTraversal();

            Console.WriteLine("\n");

            // 查找某些值
            Console.WriteLine("Search for 40: " + bst.Search(40)); // 输出: True
            Console.WriteLine("Search for 25: " + bst.Search(25)); // 输出: False

            Console.WriteLine("\n");

            // 删除某个值
            bst.Delete(50);
            Console.WriteLine("删除50后:");
            bst.InorderTraversal();
        }
    }

    /// <summary>
    /// 定义二叉搜索树的节点结构
    /// </summary>
    public class TreeNode
    {
        public int Value;
        public TreeNode Left;
        public TreeNode Right;

        public TreeNode(int value)
        {
            Value = value;
            Left = null;
            Right = null;
        }
    }

    /// <summary>
    /// 定义二叉搜索树类
    /// </summary>
    public class BinarySearchTree
    {
        private TreeNode root;

        public BinarySearchTree()
        {
            root = null;
        }

        #region 插入节点

        /// <summary>
        /// 插入新值到二叉搜索树中
        /// </summary>
        /// <param name="value">value</param>
        public void Insert(int value)
        {
            if (root == null)
            {
                root = new TreeNode(value);
            }
            else
            {
                InsertRec(root, value);
            }
        }

        private void InsertRec(TreeNode node, int value)
        {
            if (value < node.Value)
            {
                if (node.Left == null)
                {
                    node.Left = new TreeNode(value);
                }
                else
                {
                    InsertRec(node.Left, value);
                }
            }
            else if (value > node.Value)
            {
                if (node.Right == null)
                {
                    node.Right = new TreeNode(value);
                }
                else
                {
                    InsertRec(node.Right, value);
                }
            }
            else
            {
                //值已经存在于树中,不再插入
                return;
            }
        }

        #endregion

        #region 查找节点

        /// <summary>
        /// 查找某个值是否存在于二叉搜索树中
        /// </summary>
        /// <param name="value">value</param>
        /// <returns></returns>
        public bool Search(int value)
        {
            return SearchRec(root, value);
        }

        private bool SearchRec(TreeNode node, int value)
        {
            // 如果当前节点为空,表示未找到目标值
            if (node == null)
            {
                return false;
            }

            // 如果找到目标值,返回true
            if (node.Value == value)
            {
                return true;
            }

            // 递归查找左子树或右子树
            if (value < node.Value)
            {
                return SearchRec(node.Left, value);
            }
            else
            {
                return SearchRec(node.Right, value);
            }
        }

        #endregion

        #region 中序遍历

        /// <summary>
        /// 中序遍历(打印有序数组)
        /// </summary>
        public void InorderTraversal()
        {
            InorderTraversalRec(root);
        }

        private void InorderTraversalRec(TreeNode root)
        {
            if (root != null)
            {
                InorderTraversalRec(root.Left);
                Console.WriteLine(root.Value);
                InorderTraversalRec(root.Right);
            }
        }

        #endregion

        #region 删除节点

        /// <summary>
        /// 删除某个值
        /// </summary>
        /// <param name="val">val</param>
        public void Delete(int val)
        {
            root = DeleteNode(root, val);
        }

        private TreeNode DeleteNode(TreeNode node, int val)
        {
            if (node == null)
            {
                return null;
            }

            if (val < node.Value)
            {
                node.Left = DeleteNode(node.Left, val);
            }
            else if (val > node.Value)
            {
                node.Right = DeleteNode(node.Right, val);
            }
            else
            {
                // 节点有两个子节点
                if (node.Left != null && node.Right != null)
                {
                    // 使用右子树中的最小节点替换当前节点
                    TreeNode minNode = FindMin(node.Right);
                    node.Value = minNode.Value;
                    node.Right = DeleteNode(node.Right, minNode.Value);
                }
                // 节点有一个子节点或没有子节点
                else
                {
                    TreeNode? temp = node.Left != null ? node.Left : node.Right;
                    node = temp;
                }
            }

            return node;
        }

        /// <summary>
        /// 找到树中的最小节点
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private TreeNode FindMin(TreeNode node)
        {
            while (node.Left != null)
            {
                node = node.Left;
            }
            return node;
        }

        #endregion
    }
}

输出结果:

数组与搜索树的效率对比

二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能。只有在高频添加、低频查找删除数据的场景下,数组比二叉搜索树的效率更高。

二叉搜索树常见应用

  • 用作系统中的多级索引,实现高效的查找、插入、删除操作。
  • 作为某些搜索算法的底层数据结构。
  • 用于存储数据流,以保持其有序状态。

C#数据结构与算法实战入门指南

参考文章

环境信息

操作系统: Kylin Linux Advanced Server V10 (Sword)
架构:X86
MySQL版本:5.7.44

编译

  • 安装必要的依赖库和编译工具
sudo yum groupinstall 'Development Tools'
sudo yum install cmake ncurses-devel openssl-devel boost-devel libtirpc libtirpc-devel rpcgen
  • 下载 MySQL 源码

从 MySQL 官方网站或 GitHub 仓库下载 MySQL 的源码包。

wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.44.tar.gz
tar -xzvf mysql-5.7.44.tar.gz
cd mysql-5.7.44
  • 配置 CMake 选项

使用 CMake 配置 MySQL 的构建选项。可以根据需要进行定制,例如指定安装目录、禁用不需要的功能等。

cmake . \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc/mysql \
-DWITH_SSL=system \
-DWITH_ZLIB=system \
-DDEFAULT_CHARSET=utf8mb4 \
-DDEFAULT_COLLATION=utf8mb4_general_ci \
-DWITH_BOOST=boost

-DCMAKE_INSTALL_PREFIX:指定 MySQL 的安装目录。
-DMYSQL_DATADIR:指定数据存储目录。
-DSYSCONFDIR:指定配置文件目录。
-DWITH_SSL 和 -DWITH_ZLIB:指定使用系统的 SSL 和 Zlib 库。
-DDEFAULT_CHARSET 和 -DDEFAULT_COLLATION:设置默认字符集和校对规则。
-DWITH_BOOST:指定 Boost 库的位置。

关于Boost 库报错导致编译失败问题

Boost 是 MySQL 编译的必需依赖库之一。为了继续编译,CMake 需要知道 Boost 库的位置。

解决方法
选择以下两种方法之一来解决这个问题:

方法 1: 自动下载并使用 Boost 库
可以让 CMake 自动下载并使用 Boost 库。这个方法较为简单,CMake 会自动为你下载并配置 Boost 库。

cmake . \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc/mysql \
-DWITH_SSL=system \
-DWITH_ZLIB=system \
-DDEFAULT_CHARSET=utf8mb4 \
-DDEFAULT_COLLATION=utf8mb4_general_ci \
-DDOWNLOAD_BOOST=1 \
-DWITH_BOOST=/usr/local/boost
-DDOWNLOAD_BOOST=1:让 CMake 自动下载 Boost 库。
-DWITH_BOOST=/usr/local/boost:指定 Boost 库将被下载并存储的目录。可以根据需要选择不同的路径。

方法 2: 手动下载并指定 Boost 库路径
你也可以手动下载 Boost 库,并将其路径指定给 CMake。

下载 Boost 库:

wget https://boostorg.jfrog.io/artifactory/main/release/1.59.0/source/boost_1_59_0.tar.gz
tar -xzf boost_1_59_0.tar.gz

在配置 CMake 时指定 Boost 路径:

cmake . \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc/mysql \
-DWITH_SSL=system \
-DWITH_ZLIB=system \
-DDEFAULT_CHARSET=utf8mb4 \
-DDEFAULT_COLLATION=utf8mb4_general_ci \
-DWITH_BOOST=/path/to/boost_1_59_0

将 /path/to/boost_1_59_0 替换为你实际解压 Boost 的路径。

  • 编译和安装

在配置成功后,使用 make 和 make install 命令编译并安装 MySQL。

make
sudo make install

安装

  • 初始化 MySQL 数据目录

安装完成后,需要初始化 MySQL 数据目录并设置 root 密码。

cd /usr/local/mysql
sudo bin/mysqld --initialize --user=mysql

初始化完成后,记下生成的临时 root 密码,稍后将用于登录 MySQL。

  • 配置 MySQL

创建 MySQL 配置文件(如 /etc/my.cnf),并根据需要进行定制。

sudo cp support-files/my-default.cnf /etc/my.cnf
  • 启动 MySQL

设置 MySQL 为系统服务,并启动 MySQL。

sudo cp support-files/mysql.server /etc/init.d/mysql
sudo systemctl enable mysql
sudo systemctl start mysql
  • 设置 root 密码和安全配置

使用初始化时的临时密码登录 MySQL,并设置新的 root 密码。

sudo /usr/local/mysql/bin/mysql_secure_installation

根据提示完成安全配置,设置 root 密码,删除匿名用户,禁用远程 root 登录等。

  • 验证安装

通过登录 MySQL 来验证安装是否成功。

/usr/local/mysql/bin/mysql -u root -p

输入新设置的 root 密码后,进入 MySQL 命令行界面。如果成功进入,说明 MySQL 编译安装完成。

已编译完成的安装包地址:

在PHP开发中,开发者经常会遇到Malformed UTF-8 characters错误。这个错误通常是由于代码中存在无效的UTF-8字符而引起的。本篇博客将为您介绍如何解决这个问题。

什么是UTF-8字符?

UTF-8是一种用于表示Unicode字符的编码方式。它可以表示任意Unicode字符,包括ASCII字符以及其他国际字符集。在PHP中,UTF-8是默认的字符编码方式。因此,当我们处理字符串时,需要确保这些字符串是有效的UTF-8字符。

Malformed UTF-8 characters错误的原因

Malformed UTF-8 characters错误通常是在处理用户输入或从外部系统获取数据时出现的。这种错误可能是由以下几个原因引起的:

  1. 用户输入的数据包含无效的UTF-8字符。
  2. 从其他系统获取的数据包含无效的UTF-8字符。
  3. 字符串被错误地转换为了UTF-8编码。

解决Malformed UTF-8 characters错误

下面是一些解决Malformed UTF-8 characters错误的方法:

1. 使用mb_detect_encoding函数

使用mb_detect_encoding函数来检测字符串的编码类型,并确保字符串是有效的UTF-8编码。

if(mb_detect_encoding($str, 'UTF-8', true) === false){
    echo "Invalid UTF-8 string";
} else {
    // 处理字符串
}

2. 使用mb_convert_encoding函数

使用mb_convert_encoding函数将字符串转换为有效的UTF-8编码。

$str = mb_convert_encoding($str, 'UTF-8', 'auto');

3. 使用正则表达式过滤无效字符

使用正则表达式过滤字符串中的无效字符。

$str = preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '', $str);

4. 确保所有输入源都是有效的UTF-8编码

如果您从其他系统获取数据,请确保这些数据是有效的UTF-8编码。如果无法控制输入源的编码方式,可以使用相关的编码转换函数进行转换。

5. 更新PHP版本

如果您的PHP版本较旧,可能存在一些已知的UTF-8处理问题。请尽可能更新到最新的PHP版本,以确保获得修复了这些问题的版本。

总结

在PHP开发中遇到Malformed UTF-8 characters错误是非常常见的。为了解决这个问题,我们可以使用一些方法来确保处理的字符串是有效的UTF-8编码,过滤无效字符,并更新PHP版本。

参考文献:

  1. 极简博客

什么是动态代理呢?
动态代理就是在java进程运行时,通过字节码技术,动态的生成某个类的代理类。在这个代理类中,我们可以做一些额外的操作,一方面仍然保持原有的方法的能力,另外一方面还增强了这些能力。听着是不是AOP有点像,没错,动态代理就是AOP的技术基石。
在这之前我曾经写过两篇相关的文章:
https://www.cnblogs.com/jilodream/p/10611593.html 设计模式之代理模式
https://www.cnblogs.com/jilodream/p/10624940.html 设计模式之Jdk动态代理

而在实现动态代理时,一般会有两种办法:
jdk动态代理(可以点上文链接查看),以及cglib动态代理,
话不多说,我们直接来看如何使用cglib来动态代理:

例子我们还是使用和jdk动态代理相同的明星/明星代理类这个场景

pom依赖

1         <dependency>
2             <groupId>cglib</groupId>
3             <artifactId>cglib</artifactId>
4             <version>3.1</version>
5         </dependency>

明星类

1 packagecom.example.demo.learncglib;2 
3 importlombok.Data;4 importlombok.NoArgsConstructor;5 
6 /**
7 * @discription8  */
9 
10 @Data11 @NoArgsConstructor12 public class SuperStar implementsISuperStar {13 String starName;14 
15     publicSuperStar(String starName) {16         this.starName =starName;17 }18 
19 @Override20     public voidsignContract() {21         System.out.println(starName + " 签名");22         //to do sth
23         return;24 }25 
26 @Override27     public voidnegotiate() {28         System.out.println(starName + " 谈判");29         //to do sth
30         return;31 }32 
33 @Override34     publicString getStarImage() {35         System.out.println(starName + " start image");36         //to do sth
37         return "One " + starName + " Image";38 }39 }

明星类接口

1 packagecom.example.demo.learncglib;2 
3 public interfaceISuperStar4 {5     /**
6 * 签约7      */
8     voidsignContract();9 
10     voidnegotiate();11 
12 String getStarImage();13 }

代理工厂

1 packagecom.example.demo.learncglib;2 
3 
4 importnet.sf.cglib.core.DebuggingClassWriter;5 importnet.sf.cglib.proxy.Enhancer;6 importnet.sf.cglib.proxy.MethodInterceptor;7 importnet.sf.cglib.proxy.MethodProxy;8 
9 importjava.lang.reflect.Method;10 
11 /**
12 * @discription13  */
14 public class ProxyFactory implementsMethodInterceptor {15 
16     privateString starName;17 
18     publicSuperStar create(String starName) {19         System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\code\\common\\learn-design" +
20 "-pattern\\target\\cglib");
21 22 this.starName =starName;23 Enhancer enhancer = newEnhancer();24 enhancer.setSuperclass(SuperStar.class);25 enhancer.setCallback(this);26 SuperStar proxy =(SuperStar) enhancer.create();27 proxy.starName =starName;28 returnproxy;29 }30 31 32 @Override33 public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throwsThrowable {34 System.out.println(starName + "的代理人开始组织活动");35 Object obj =methodProxy.invokeSuper(o, objects);36 System.out.println(starName + "的代理人结束组织活动");37 returnobj;38 }39 }

主类

1 packagecom.example.demo.learncglib;2 
3 /**
4 * @discription5  */
6 public classCglibMain {7     public static voidmain(String[] args) {8         ProxyFactory factory = newProxyFactory();9         SuperStar superStar = factory.create("messi");10 superStar.signContract();11 superStar.negotiate();12         String image=superStar.getStarImage();13         System.out.println("we get a image: "+image);14 }15 
16 
17 }

运行效果

messi的代理人开始组织活动
messi 签名
messi的代理人结束组织活动
messi的代理人开始组织活动
messi 谈判
messi的代理人结束组织活动
messi的代理人开始组织活动
messi start image
messi的代理人结束组织活动
we get a image: One messi Image
Disconnected from the target VM, address:
'127.0.0.1:64165', transport: 'socket'

生成代理类的核心逻辑在com.example.demo.learncglib.ProxyFactory#create方法中:

我们首先声明一个增强器:Enhancer enhancer
接着设置代理类父类:SuperStar.class
接着设置回调类(包含增强方法的类):? implements MethodInterceptor
最后调用增强类的创建方法就生成好了:enhancer.create()

整体的流程和jdk动态代理很像,

不同点是jdk动态代理是根据接口,动态的生成实现类,代理类和被代理类均为接口实现类,是“兄弟关系”,被代理的方法就是接口中公开的方法。
而cglib动态代理是将被代理类作为父类,派生出子类,代理类和被代理类均为继承关系,是“父子关系”,被代理的方法就是父类中公开的方法。
如下图:

其实细细想想也很正常,我们想要间接的动态的获取到某个类中公开的方法,有两种途径,第一种是继承自它,那么它所有的公开方法我们都能继续持有(cglib的思路)。第二种就是和他实现相同的约定(接口),那么它多有开发的协议,我们也就能动态的获取到了(jdk动态代理的思路)。

说了这么多来讲讲cglib方式的局限性,主要还是和继承有关系:
1、无法动态代理final方法,因为子类无法复写。

2、无法动态代理static,因为子类无法复写。


jdk动态代理和cglib可以说是各有优劣,很多人说经过jdk动态代理的速度优化,spring 目前已经默认采用jdk动态代理了,所以jdk动态代理更好。

这里我要说两点,

1、设计和实现是两回事,未来cglib的实现思路经过优化,又胜出了,也并不能说明cglib的设计方案更好;

2、目前的Springboot最新版本又采用了cglib作为默认的aop实现方式,这也并不能说明cglib就比jdk动态代理方式要强了。


ps:如果想要将运行时动态生成的class文件保存到磁盘中,可以在执行的代码出添加,如上文代码示例的红色字体处:
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\code\\common\\learn-design-pattern\\target\\cglib");