ZigでOSを作るシリーズ
| Part1 基本 | Part2 ブート | Part3 割り込み | Part4 メモリ | Part5 プロセス | Part6 FS |
|---|---|---|---|---|---|
| ✅ Done | ✅ Done | ✅ Done | 👈 Now | - | - |
はじめに
前回は割り込み処理を実装した。
今回はメモリ管理!物理メモリアロケータ、仮想メモリ管理、そしてZigのアロケータインターフェースを使った動的メモリ確保を実現する。
メモリ管理はOSの中でも特に面白い部分なんだよね。ここがしっかりできれば、だいたいのことはできるようになるよ。
メモリ管理の全体像
┌─────────────────────────────────────────────────────────────────┐
│ メモリ管理の階層構造 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Application / Kernel Code │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Zig Allocator Interface │ │
│ │ (std.mem.Allocator compatible) │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Virtual Memory Manager (VMM) │ │
│ │ Page Table Management │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Physical Memory Allocator (PMM) │ │
│ │ Bitmap / Buddy Allocator │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Physical Memory (RAM) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
物理メモリアロケータ(Bitmap)
まずは単純なビットマップアロケータから作っていくよ。
// pmm.zig - Physical Memory Manager
const std = @import("std");
const PAGE_SIZE: usize = 4096;
const MAX_MEMORY: usize = 4 * 1024 * 1024 * 1024; // 4GB
const BITMAP_SIZE: usize = MAX_MEMORY / PAGE_SIZE / 8;
var bitmap: [BITMAP_SIZE]u8 = [_]u8{0xFF} ** BITMAP_SIZE; // 全て使用中で初期化
var total_memory: usize = 0;
var used_memory: usize = 0;
pub fn init(mmap_addr: u64, mmap_length: u64) void {
// メモリマップを解析して利用可能な領域を記録
var entry_ptr: [*]const MmapEntry = @ptrFromInt(@as(usize, @truncate(mmap_addr)));
const end_ptr = @as(usize, @truncate(mmap_addr + mmap_length));
while (@intFromPtr(entry_ptr) < end_ptr) {
const entry = entry_ptr[0];
if (entry.type == 1) { // Available
const base = alignUp(entry.base_addr, PAGE_SIZE);
const end = alignDown(entry.base_addr + entry.length, PAGE_SIZE);
if (end > base) {
markRegionFree(base, end - base);
total_memory += end - base;
}
}
entry_ptr = @ptrFromInt(@intFromPtr(entry_ptr) + entry.size + 4);
}
// カーネル領域を予約(1MB - 4MB)
markRegionUsed(0x100000, 3 * 1024 * 1024);
}
const MmapEntry = extern struct {
size: u32,
base_addr: u64,
length: u64,
type: u32,
};
fn markRegionFree(base: u64, length: u64) void {
const start_page = @as(usize, @truncate(base / PAGE_SIZE));
const page_count = @as(usize, @truncate(length / PAGE_SIZE));
for (start_page..start_page + page_count) |page| {
clearBit(page);
}
}
fn markRegionUsed(base: u64, length: u64) void {
const start_page = @as(usize, @truncate(base / PAGE_SIZE));
const page_count = @as(usize, @truncate(length / PAGE_SIZE));
for (start_page..start_page + page_count) |page| {
setBit(page);
}
used_memory += length;
}
fn setBit(page: usize) void {
bitmap[page / 8] |= @as(u8, 1) << @truncate(page % 8);
}
fn clearBit(page: usize) void {
bitmap[page / 8] &= ~(@as(u8, 1) << @truncate(page % 8));
}
fn testBit(page: usize) bool {
return (bitmap[page / 8] & (@as(u8, 1) << @truncate(page % 8))) != 0;
}
// 単一ページを割り当て
pub fn allocatePage() ?u64 {
for (0..BITMAP_SIZE * 8) |page| {
if (!testBit(page)) {
setBit(page);
used_memory += PAGE_SIZE;
return @as(u64, page) * PAGE_SIZE;
}
}
return null;
}
// 連続した複数ページを割り当て
pub fn allocatePages(count: usize) ?u64 {
var consecutive: usize = 0;
var start_page: usize = 0;
for (0..BITMAP_SIZE * 8) |page| {
if (!testBit(page)) {
if (consecutive == 0) {
start_page = page;
}
consecutive += 1;
if (consecutive == count) {
// 見つかった!全部使用中にする
for (start_page..start_page + count) |p| {
setBit(p);
}
used_memory += count * PAGE_SIZE;
return @as(u64, start_page) * PAGE_SIZE;
}
} else {
consecutive = 0;
}
}
return null;
}
// ページを解放
pub fn freePage(addr: u64) void {
const page = @as(usize, @truncate(addr / PAGE_SIZE));
if (testBit(page)) {
clearBit(page);
used_memory -= PAGE_SIZE;
}
}
pub fn freePages(addr: u64, count: usize) void {
for (0..count) |i| {
freePage(addr + i * PAGE_SIZE);
}
}
pub fn getTotalMemory() usize {
return total_memory;
}
pub fn getUsedMemory() usize {
return used_memory;
}
pub fn getFreeMemory() usize {
return total_memory - used_memory;
}
fn alignUp(addr: u64, alignment: u64) u64 {
return (addr + alignment - 1) & ~(alignment - 1);
}
fn alignDown(addr: u64, alignment: u64) u64 {
return addr & ~(alignment - 1);
}
仮想メモリマネージャ
ページテーブルを管理して、仮想アドレスと物理アドレスをマッピングするよ。
// vmm.zig - Virtual Memory Manager
const pmm = @import("pmm.zig");
const PAGE_SIZE: usize = 4096;
// ページテーブルエントリのフラグ
const PAGE_PRESENT: u64 = 1 << 0;
const PAGE_WRITABLE: u64 = 1 << 1;
const PAGE_USER: u64 = 1 << 2;
const PAGE_WRITE_THROUGH: u64 = 1 << 3;
const PAGE_CACHE_DISABLE: u64 = 1 << 4;
const PAGE_ACCESSED: u64 = 1 << 5;
const PAGE_DIRTY: u64 = 1 << 6;
const PAGE_HUGE: u64 = 1 << 7;
const PAGE_GLOBAL: u64 = 1 << 8;
const PAGE_NO_EXECUTE: u64 = 1 << 63;
const PageTable = *align(PAGE_SIZE) [512]u64;
var kernel_pml4: PageTable = undefined;
pub fn init() void {
// カーネル用のPML4を作成
const pml4_phys = pmm.allocatePage() orelse unreachable;
kernel_pml4 = @ptrFromInt(@as(usize, @truncate(pml4_phys)));
// ゼロクリア
for (kernel_pml4) |*entry| {
entry.* = 0;
}
// Identity mapping(最初の4GB)
identityMap(0, 4 * 1024 * 1024 * 1024);
// PML4をCR3に設定
loadPageTable(pml4_phys);
}
fn identityMap(start: u64, size: u64) void {
var addr = start;
while (addr < start + size) : (addr += PAGE_SIZE) {
mapPage(addr, addr, PAGE_PRESENT | PAGE_WRITABLE);
}
}
pub fn mapPage(virt: u64, phys: u64, flags: u64) void {
const pml4_idx = (virt >> 39) & 0x1FF;
const pdpt_idx = (virt >> 30) & 0x1FF;
const pd_idx = (virt >> 21) & 0x1FF;
const pt_idx = (virt >> 12) & 0x1FF;
// PML4 → PDPT
if (kernel_pml4[pml4_idx] & PAGE_PRESENT == 0) {
const pdpt_phys = pmm.allocatePage() orelse return;
kernel_pml4[pml4_idx] = pdpt_phys | PAGE_PRESENT | PAGE_WRITABLE;
zeroPage(pdpt_phys);
}
const pdpt: PageTable = @ptrFromInt(@as(usize, @truncate(kernel_pml4[pml4_idx] & 0x000FFFFFFFFFF000)));
// PDPT → PD
if (pdpt[pdpt_idx] & PAGE_PRESENT == 0) {
const pd_phys = pmm.allocatePage() orelse return;
pdpt[pdpt_idx] = pd_phys | PAGE_PRESENT | PAGE_WRITABLE;
zeroPage(pd_phys);
}
const pd: PageTable = @ptrFromInt(@as(usize, @truncate(pdpt[pdpt_idx] & 0x000FFFFFFFFFF000)));
// PD → PT
if (pd[pd_idx] & PAGE_PRESENT == 0) {
const pt_phys = pmm.allocatePage() orelse return;
pd[pd_idx] = pt_phys | PAGE_PRESENT | PAGE_WRITABLE;
zeroPage(pt_phys);
}
const pt: PageTable = @ptrFromInt(@as(usize, @truncate(pd[pd_idx] & 0x000FFFFFFFFFF000)));
// PT → Physical Page
pt[pt_idx] = phys | flags;
// TLBをフラッシュ
invlpg(virt);
}
pub fn unmapPage(virt: u64) void {
const pml4_idx = (virt >> 39) & 0x1FF;
const pdpt_idx = (virt >> 30) & 0x1FF;
const pd_idx = (virt >> 21) & 0x1FF;
const pt_idx = (virt >> 12) & 0x1FF;
if (kernel_pml4[pml4_idx] & PAGE_PRESENT == 0) return;
const pdpt: PageTable = @ptrFromInt(@as(usize, @truncate(kernel_pml4[pml4_idx] & 0x000FFFFFFFFFF000)));
if (pdpt[pdpt_idx] & PAGE_PRESENT == 0) return;
const pd: PageTable = @ptrFromInt(@as(usize, @truncate(pdpt[pdpt_idx] & 0x000FFFFFFFFFF000)));
if (pd[pd_idx] & PAGE_PRESENT == 0) return;
const pt: PageTable = @ptrFromInt(@as(usize, @truncate(pd[pd_idx] & 0x000FFFFFFFFFF000)));
pt[pt_idx] = 0;
invlpg(virt);
}
pub fn translateAddress(virt: u64) ?u64 {
const pml4_idx = (virt >> 39) & 0x1FF;
const pdpt_idx = (virt >> 30) & 0x1FF;
const pd_idx = (virt >> 21) & 0x1FF;
const pt_idx = (virt >> 12) & 0x1FF;
const offset = virt & 0xFFF;
if (kernel_pml4[pml4_idx] & PAGE_PRESENT == 0) return null;
const pdpt: PageTable = @ptrFromInt(@as(usize, @truncate(kernel_pml4[pml4_idx] & 0x000FFFFFFFFFF000)));
if (pdpt[pdpt_idx] & PAGE_PRESENT == 0) return null;
const pd: PageTable = @ptrFromInt(@as(usize, @truncate(pdpt[pdpt_idx] & 0x000FFFFFFFFFF000)));
if (pd[pd_idx] & PAGE_PRESENT == 0) return null;
const pt: PageTable = @ptrFromInt(@as(usize, @truncate(pd[pd_idx] & 0x000FFFFFFFFFF000)));
if (pt[pt_idx] & PAGE_PRESENT == 0) return null;
return (pt[pt_idx] & 0x000FFFFFFFFFF000) + offset;
}
fn zeroPage(phys: u64) void {
const ptr: [*]u8 = @ptrFromInt(@as(usize, @truncate(phys)));
for (0..PAGE_SIZE) |i| {
ptr[i] = 0;
}
}
fn loadPageTable(pml4_phys: u64) void {
asm volatile ("mov %[pml4], %%cr3"
:
: [pml4] "r" (pml4_phys),
);
}
fn invlpg(virt: u64) void {
asm volatile ("invlpg (%[virt])"
:
: [virt] "r" (virt),
: "memory"
);
}
ヒープアロケータ
Zigのアロケータインターフェースに対応したヒープを実装。
// heap.zig - Kernel Heap Allocator
const std = @import("std");
const pmm = @import("pmm.zig");
const vmm = @import("vmm.zig");
const PAGE_SIZE: usize = 4096;
const HEAP_START: usize = 0xFFFF_8000_0000_0000; // カーネルヒープ領域
const HEAP_INITIAL_SIZE: usize = 16 * 1024 * 1024; // 16MB
// フリーリストのブロックヘッダ
const BlockHeader = struct {
size: usize,
next: ?*BlockHeader,
is_free: bool,
};
var heap_start: usize = HEAP_START;
var heap_end: usize = HEAP_START;
var free_list: ?*BlockHeader = null;
pub fn init() void {
// 初期ヒープ領域をマップ
expandHeap(HEAP_INITIAL_SIZE);
}
fn expandHeap(size: usize) void {
const pages = (size + PAGE_SIZE - 1) / PAGE_SIZE;
for (0..pages) |_| {
const phys = pmm.allocatePage() orelse return;
vmm.mapPage(heap_end, phys, 0x03); // Present | Writable
heap_end += PAGE_SIZE;
}
// 新しい空き領域をフリーリストに追加
const new_block: *BlockHeader = @ptrFromInt(heap_end - size);
new_block.* = .{
.size = size - @sizeOf(BlockHeader),
.next = free_list,
.is_free = true,
};
free_list = new_block;
}
// First-fit アロケーション
fn findFreeBlock(size: usize) ?*BlockHeader {
var current = free_list;
while (current) |block| {
if (block.is_free and block.size >= size) {
return block;
}
current = block.next;
}
return null;
}
pub fn alloc(size: usize) ?[*]u8 {
const aligned_size = alignUp(size, 16);
const total_size = aligned_size + @sizeOf(BlockHeader);
// フリーリストから探す
if (findFreeBlock(aligned_size)) |block| {
// ブロックを分割できるかチェック
if (block.size > total_size + 64) {
// 分割
const new_block: *BlockHeader = @ptrFromInt(@intFromPtr(block) + total_size);
new_block.* = .{
.size = block.size - total_size,
.next = block.next,
.is_free = true,
};
block.size = aligned_size;
block.next = new_block;
}
block.is_free = false;
return @ptrFromInt(@intFromPtr(block) + @sizeOf(BlockHeader));
}
// 新しいページを割り当て
expandHeap(alignUp(total_size, PAGE_SIZE));
return alloc(size); // 再試行
}
pub fn free(ptr: [*]u8) void {
const header: *BlockHeader = @ptrFromInt(@intFromPtr(ptr) - @sizeOf(BlockHeader));
header.is_free = true;
// 隣接する空きブロックと結合(簡略化版)
coalesceFreeBlocks();
}
fn coalesceFreeBlocks() void {
var current = free_list;
while (current) |block| {
if (block.next) |next| {
if (block.is_free and next.is_free) {
// 結合
block.size += next.size + @sizeOf(BlockHeader);
block.next = next.next;
continue; // もう一度同じブロックをチェック
}
}
current = block.next;
}
}
fn alignUp(value: usize, alignment: usize) usize {
return (value + alignment - 1) & ~(alignment - 1);
}
// Zig std.mem.Allocator インターフェース
pub const kernel_allocator = std.mem.Allocator{
.ptr = undefined,
.vtable = &.{
.alloc = zigAlloc,
.resize = zigResize,
.free = zigFree,
},
};
fn zigAlloc(_: *anyopaque, len: usize, _: u8, _: usize) ?[*]u8 {
return alloc(len);
}
fn zigResize(_: *anyopaque, _: []u8, _: u8, _: usize, _: usize) bool {
// リサイズは未実装
return false;
}
fn zigFree(_: *anyopaque, buf: []u8, _: u8, _: usize) void {
free(buf.ptr);
}
バディアロケータ(高度版)
効率的なメモリ管理のためのバディシステム。
// buddy.zig - Buddy Allocator
const std = @import("std");
const pmm = @import("pmm.zig");
const PAGE_SIZE: usize = 4096;
const MIN_ORDER: u5 = 0; // 4KB
const MAX_ORDER: u5 = 10; // 4MB
const FreeList = struct {
head: ?*BuddyBlock,
};
const BuddyBlock = struct {
next: ?*BuddyBlock,
order: u5,
};
var free_lists: [MAX_ORDER + 1]FreeList = [_]FreeList{.{ .head = null }} ** (MAX_ORDER + 1);
var base_addr: usize = 0;
var total_pages: usize = 0;
pub fn init(start: usize, size: usize) void {
base_addr = start;
total_pages = size / PAGE_SIZE;
// 最大サイズのブロックとして追加
var remaining = total_pages;
var current = start;
while (remaining > 0) {
const order = findLargestOrder(remaining);
const block: *BuddyBlock = @ptrFromInt(current);
block.* = .{
.next = free_lists[order].head,
.order = order,
};
free_lists[order].head = block;
const block_pages = @as(usize, 1) << order;
current += block_pages * PAGE_SIZE;
remaining -= block_pages;
}
}
fn findLargestOrder(pages: usize) u5 {
var order: u5 = MAX_ORDER;
while (order > MIN_ORDER) : (order -= 1) {
if ((@as(usize, 1) << order) <= pages) {
return order;
}
}
return MIN_ORDER;
}
pub fn allocate(size: usize) ?[*]u8 {
// 必要なオーダーを計算
const pages = (size + PAGE_SIZE - 1) / PAGE_SIZE;
var order: u5 = MIN_ORDER;
while ((@as(usize, 1) << order) < pages and order < MAX_ORDER) {
order += 1;
}
return allocateOrder(order);
}
fn allocateOrder(order: u5) ?[*]u8 {
// このオーダーのフリーリストから取得
if (free_lists[order].head) |block| {
free_lists[order].head = block.next;
return @ptrCast(block);
}
// より大きなブロックを分割
if (order < MAX_ORDER) {
if (allocateOrder(order + 1)) |larger_ptr| {
const larger: usize = @intFromPtr(larger_ptr);
// 半分をフリーリストに戻す
const buddy_addr = larger + ((@as(usize, 1) << order) * PAGE_SIZE);
const buddy: *BuddyBlock = @ptrFromInt(buddy_addr);
buddy.* = .{
.next = free_lists[order].head,
.order = order,
};
free_lists[order].head = buddy;
return @ptrFromInt(larger);
}
}
return null;
}
pub fn deallocate(ptr: [*]u8, size: usize) void {
const pages = (size + PAGE_SIZE - 1) / PAGE_SIZE;
var order: u5 = MIN_ORDER;
while ((@as(usize, 1) << order) < pages and order < MAX_ORDER) {
order += 1;
}
freeOrder(@intFromPtr(ptr), order);
}
fn freeOrder(addr: usize, order: u5) void {
// バディを探す
const buddy_addr = addr ^ ((@as(usize, 1) << order) * PAGE_SIZE);
// バディがフリーリストにあるかチェック
var prev: ?*?*BuddyBlock = null;
var current = free_lists[order].head;
while (current) |block| {
if (@intFromPtr(block) == buddy_addr) {
// バディを見つけた!結合する
if (prev) |p| {
p.* = block.next;
} else {
free_lists[order].head = block.next;
}
// 結合して一つ上のオーダーへ
const merged_addr = @min(addr, buddy_addr);
if (order < MAX_ORDER) {
freeOrder(merged_addr, order + 1);
} else {
// 最大オーダーに達した
const merged: *BuddyBlock = @ptrFromInt(merged_addr);
merged.* = .{
.next = free_lists[order].head,
.order = order,
};
free_lists[order].head = merged;
}
return;
}
prev = &block.next;
current = block.next;
}
// バディが見つからない - そのまま追加
const block: *BuddyBlock = @ptrFromInt(addr);
block.* = .{
.next = free_lists[order].head,
.order = order,
};
free_lists[order].head = block;
}
pub fn getFragmentation() f32 {
var total_free: usize = 0;
var largest_free: usize = 0;
for (free_lists, 0..) |list, order| {
var current = list.head;
while (current) |block| {
const size = (@as(usize, 1) << @truncate(order)) * PAGE_SIZE;
total_free += size;
if (size > largest_free) {
largest_free = size;
}
current = block.next;
}
}
if (total_free == 0) return 0;
return 1.0 - @as(f32, @floatFromInt(largest_free)) / @as(f32, @floatFromInt(total_free));
}
使用例:動的配列
// main.zig
const std = @import("std");
const heap = @import("heap.zig");
const vga = @import("vga.zig");
pub fn dynamicArrayDemo() void {
const allocator = heap.kernel_allocator;
// 動的配列を作成
var list = std.ArrayList(u32).init(allocator);
defer list.deinit();
// 要素を追加
list.append(10) catch {};
list.append(20) catch {};
list.append(30) catch {};
vga.print("Dynamic Array: ");
for (list.items) |item| {
printNumber(item);
vga.print(" ");
}
vga.print("\n");
// HashMap
var map = std.AutoHashMap([]const u8, u32).init(allocator);
defer map.deinit();
map.put("one", 1) catch {};
map.put("two", 2) catch {};
map.put("three", 3) catch {};
if (map.get("two")) |value| {
vga.print("map['two'] = ");
printNumber(value);
vga.print("\n");
}
}
fn printNumber(n: u32) void {
var buf: [10]u8 = undefined;
var i: usize = buf.len;
var num = n;
if (num == 0) {
vga.putChar('0');
return;
}
while (num > 0) {
i -= 1;
buf[i] = @truncate('0' + (num % 10));
num /= 10;
}
for (buf[i..]) |c| {
vga.putChar(c);
}
}
メモリ情報表示
// memory_info.zig
const vga = @import("vga.zig");
const pmm = @import("pmm.zig");
pub fn printMemoryInfo() void {
vga.print("\n=== Memory Information ===\n");
const total = pmm.getTotalMemory();
const used = pmm.getUsedMemory();
const free = pmm.getFreeMemory();
vga.print("Total: ");
printSize(total);
vga.print("\n");
vga.print("Used: ");
printSize(used);
vga.print("\n");
vga.print("Free: ");
printSize(free);
vga.print("\n");
// 使用率バー
vga.print("\nUsage: [");
const bar_width = 40;
const used_bars = (used * bar_width) / total;
for (0..bar_width) |i| {
if (i < used_bars) {
vga.putChar('#');
} else {
vga.putChar('-');
}
}
vga.print("] ");
printNumber((used * 100) / total);
vga.print("%\n");
}
fn printSize(bytes: usize) void {
if (bytes >= 1024 * 1024 * 1024) {
printNumber(bytes / (1024 * 1024 * 1024));
vga.print(" GB");
} else if (bytes >= 1024 * 1024) {
printNumber(bytes / (1024 * 1024));
vga.print(" MB");
} else if (bytes >= 1024) {
printNumber(bytes / 1024);
vga.print(" KB");
} else {
printNumber(bytes);
vga.print(" B");
}
}
fn printNumber(n: usize) void {
var buf: [20]u8 = undefined;
var i: usize = buf.len;
var num = n;
if (num == 0) {
vga.putChar('0');
return;
}
while (num > 0) {
i -= 1;
buf[i] = @truncate('0' + (num % 10));
num /= 10;
}
for (buf[i..]) |c| {
vga.putChar(c);
}
}
まとめ
今回実装したこと:
| コンポーネント | 説明 |
|---|---|
| PMM(Bitmap) | 物理ページの管理 |
| VMM | ページテーブル操作、仮想メモリマッピング |
| ヒープアロケータ | First-fit方式の動的メモリ確保 |
| バディアロケータ | 効率的なメモリ管理 |
| Zigインターフェース | std.mem.Allocator互換 |
┌─────────────────────────────────────────────────────────────┐
│ メモリレイアウト │
├─────────────────────────────────────────────────────────────┤
│ 0x0000_0000 ┌──────────────────────────────────┐ │
│ │ Reserved (1MB) │ │
│ 0x0010_0000 ├──────────────────────────────────┤ │
│ │ Kernel Code │ │
│ 0x0040_0000 ├──────────────────────────────────┤ │
│ │ Physical Page Pool │ │
│ │ (PMM managed) │ │
│ └──────────────────────────────────┘ │
│ │
│ Virtual Address Space: │
│ 0xFFFF_8000_0000_0000 ┌────────────────────────┐ │
│ │ Kernel Heap │ │
│ │ (16MB initial) │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
次回はプロセス管理を実装するよ。タスクスイッチとスケジューリングで、マルチタスクが動くようになるよ!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!