[kernel] 帶著問題看源碼 —— 進程 ID 是如何分配的

来源:https://www.cnblogs.com/goodcitizen/p/18130888/how_linux_allocate_pid
-Advertisement-
Play Games

Linux 可用 pid 上限是多少?如何提升上限?為何提升上限可以實時生效?Linux 底層如何實現 pid 快速分配與歸還?這種實現為何只需要極少的記憶體開銷?本文通過閱讀 Linux 內核源碼,一一為你揭秘 ...


前言

在《[apue] 進程式控制制那些事兒 》一文中,曾提到進程 ID 並不是唯一的,在整個系統運行期間一個進程 ID 可能會出現好多次。

> ./pid
fork and exec child 18687
[18687] child running
wait child 18687 return 0
fork and exec child 18688
[18688] child running
wait child 18688 return 0
fork and exec child 18689
...
wait child 18683 return 0
fork and exec child 18684
[18684] child running
wait child 18684 return 0
fork and exec child 18685
[18685] child running
wait child 18685 return 0
fork and exec child 18687
[18687] child running
wait child 18687 return 0
duplicated pid find: 18687, total 31930, elapse 8

如果一直不停的 fork 子進程,在 Linux 上大約 8 秒就會得到重覆的 pid,在 macOS 上大約是一分多鐘。

...
[32765] child running
wait child 32765 return 0
fork and exec child 32766
[32766] child running
wait child 32766 return 0
fork and exec child 32767
[32767] child running
wait child 32767 return 0
fork and exec child 300
[300] child running
wait child 300 return 0
fork and exec child 313
[313] child running
wait child 313 return 0
fork and exec child 314
[314] child running
wait child 314 return 0
...

並且在 Linux 上 pid 的分配範圍是 [300, 32768),約 3W 個;在 macOS 上是 [100,99999),約 10W 個。

為何會產生這種差異?Linux 上是如何檢索並分配空閑 pid 的?帶著這個問題,找出系統對應的內核源碼看個究竟。

源碼分析

和《[kernel] 帶著問題看源碼 —— setreuid 何時更新 saved-set-uid (SUID)》一樣,這裡使用 bootlin 查看內核 3.10.0 版本源碼,關於 bootlin 的簡單介紹也可以參考那篇文章。

進程 ID 是在 fork 時分配的,所以先搜索 sys_fork:

整個搜索過程大概是 sys_fork -> do_fork -> copy_process -> alloc_pid -> alloc_pidmap,下麵分別說明。

copy_process

sys_fork & do_fork 都比較簡單,其中 do_fork 主要調用 copy_process 複製進程內容,這個函數很長,直接搜索關鍵字 pid :

查看代碼
 /*
 * This creates a new process as a copy of the old one,
 * but does not actually start it yet.
 *
 * It copies the registers, and all the appropriate
 * parts of the process environment (as per the clone
 * flags). The actual kick-off is left to the caller.
 */
static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
    int retval;
    struct task_struct *p;

    ...
    /* copy all the process information */
    retval = copy_semundo(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_audit;
    retval = copy_files(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_semundo;
    retval = copy_fs(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_files;
    retval = copy_sighand(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_fs;
    retval = copy_signal(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_sighand;
    retval = copy_mm(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_signal;
    retval = copy_namespaces(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_mm;
    retval = copy_io(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_namespaces;
    retval = copy_thread(clone_flags, stack_start, stack_size, p);
    if (retval)
        goto bad_fork_cleanup_io;

    if (pid != &init_struct_pid) {
        retval = -ENOMEM;
        pid = alloc_pid(p->nsproxy->pid_ns);
        if (!pid)
            goto bad_fork_cleanup_io;
    }

    p->pid = pid_nr(pid);
    p->tgid = p->pid;

    ...
    if (likely(p->pid)) {
        ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

        if (thread_group_leader(p)) {
            if (is_child_reaper(pid)) {
                ns_of_pid(pid)->child_reaper = p;
                p->signal->flags |= SIGNAL_UNKILLABLE;
            }

            p->signal->leader_pid = pid;
            p->signal->tty = tty_kref_get(current->signal->tty);
            attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
            attach_pid(p, PIDTYPE_SID, task_session(current));
            list_add_tail(&p->sibling, &p->real_parent->children);
            list_add_tail_rcu(&p->tasks, &init_task.tasks);
            __this_cpu_inc(process_counts);
        }
        attach_pid(p, PIDTYPE_PID, pid);
        nr_threads++;
    }

    ...
    return p;

bad_fork_free_pid:
    if (pid != &init_struct_pid)
        free_pid(pid);
bad_fork_cleanup_io:
    if (p->io_context)
        exit_io_context(p);
bad_fork_cleanup_namespaces:
    exit_task_namespaces(p);
bad_fork_cleanup_mm:
    if (p->mm)
        mmput(p->mm);
bad_fork_cleanup_signal:
    if (!(clone_flags & CLONE_THREAD))
        free_signal_struct(p->signal);
bad_fork_cleanup_sighand:
    __cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
    exit_fs(p); /* blocking */
bad_fork_cleanup_files:
    exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
    exit_sem(p);
bad_fork_cleanup_audit:
    audit_free(p);
bad_fork_cleanup_policy:
    perf_event_free_task(p);
    if (clone_flags & CLONE_THREAD)
        threadgroup_change_end(current);
    cgroup_exit(p, 0);
    delayacct_tsk_free(p);
    module_put(task_thread_info(p)->exec_domain->module);
bad_fork_cleanup_count:
    atomic_dec(&p->cred->user->processes);
    exit_creds(p);
bad_fork_free:
    free_task(p);
fork_out:
    return ERR_PTR(retval);
}

copy_process 的核心就是各種資源的拷貝,表現為 copy_xxx 函數的調用,如果有對應的 copy 函數失敗了,會 goto 到整個函數末尾的 bad_fork_cleanup_xxx 標簽進行清理,copy 調用與清理順序是相反的,保證路徑上的所有資源能得到正確釋放。

在 copy_xxx 調用的末尾,搜到了一段與 pid 分配相關的代碼:

    if (pid != &init_struct_pid) {
        retval = -ENOMEM;
        pid = alloc_pid(p->nsproxy->pid_ns);
        if (!pid)
            goto bad_fork_cleanup_io;
    }

    p->pid = pid_nr(pid);
    p->tgid = p->pid;

首先判斷進程不是 init 進程才給分配 pid (參數 pid 在 do_fork 調用 copy_process 時設置為 NULL,所以這裡 if 條件為 true 可以進入),然後通過 alloc_pid 為進程分配新的 pid。

在繼續分析 alloc_pid 之前,先把搜索到的另一段包含 pid 代碼瀏覽下:

    if (likely(p->pid)) {
        ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

        if (thread_group_leader(p)) {
            if (is_child_reaper(pid)) {
                ns_of_pid(pid)->child_reaper = p;
                p->signal->flags |= SIGNAL_UNKILLABLE;
            }

            p->signal->leader_pid = pid;
            p->signal->tty = tty_kref_get(current->signal->tty);
            attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
            attach_pid(p, PIDTYPE_SID, task_session(current));
            list_add_tail(&p->sibling, &p->real_parent->children);
            list_add_tail_rcu(&p->tasks, &init_task.tasks);
            __this_cpu_inc(process_counts);
        }
        attach_pid(p, PIDTYPE_PID, pid);
        nr_threads++;
    }

如果 pid 分配成功,將它們設置到進程結構中以便生效,主要工作在 attach_pid,限於篇幅就不深入研究了。

alloc_pid

代碼不長,就不刪減了:

查看代碼
 struct pid *alloc_pid(struct pid_namespace *ns)
{
    struct pid *pid;
    enum pid_type type;
    int i, nr;
    struct pid_namespace *tmp;
    struct upid *upid;

    pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
    if (!pid)
        goto out;

    tmp = ns;
    pid->level = ns->level;
    for (i = ns->level; i >= 0; i--) {
        nr = alloc_pidmap(tmp);
        if (nr < 0)
            goto out_free;

        pid->numbers[i].nr = nr;
        pid->numbers[i].ns = tmp;
        tmp = tmp->parent;
    }

    if (unlikely(is_child_reaper(pid))) {
        if (pid_ns_prepare_proc(ns))
            goto out_free;
    }

    get_pid_ns(ns);
    atomic_set(&pid->count, 1);
    for (type = 0; type < PIDTYPE_MAX; ++type)
        INIT_HLIST_HEAD(&pid->tasks[type]);

    upid = pid->numbers + ns->level;
    spin_lock_irq(&pidmap_lock);
    if (!(ns->nr_hashed & PIDNS_HASH_ADDING))
        goto out_unlock;
    for ( ; upid >= pid->numbers; --upid) {
        hlist_add_head_rcu(&upid->pid_chain,
                &pid_hash[pid_hashfn(upid->nr, upid->ns)]);
        upid->ns->nr_hashed++;
    }
    spin_unlock_irq(&pidmap_lock);

out:
    return pid;

out_unlock:
    spin_unlock_irq(&pidmap_lock);
out_free:
    while (++i <= ns->level)
        free_pidmap(pid->numbers + i);

    kmem_cache_free(ns->pid_cachep, pid);
    pid = NULL;
    goto out;
}

代碼不長但是看得雲里霧裡,查找了一些相關資料,3.10 內核為了支持容器,通過各種 namespace 做資源隔離,與 pid 相關的就是 pid_namespace 啦。這東西還可以嵌套、還可以對上層可見,所以做的很複雜,可以開一個單獨的文章去講它了。這裡為了不偏離主題,暫時擱置,直接看 alloc_pidmap 完事兒,感興趣的可以參考附錄 6。

alloc_pidmap

到這裡才涉及到本文核心,每一行都很重要,就不做刪減了:

static int alloc_pidmap(struct pid_namespace *pid_ns)
{
    int i, offset, max_scan, pid, last = pid_ns->last_pid;
    struct pidmap *map;

    pid = last + 1;
    if (pid >= pid_max)
        pid = RESERVED_PIDS;
    offset = pid & BITS_PER_PAGE_MASK;
    map = &pid_ns->pidmap[pid/BITS_PER_PAGE];
    /*
     * If last_pid points into the middle of the map->page we
     * want to scan this bitmap block twice, the second time
     * we start with offset == 0 (or RESERVED_PIDS).
     */
    max_scan = DIV_ROUND_UP(pid_max, BITS_PER_PAGE) - !offset;
    for (i = 0; i <= max_scan; ++i) {
        if (unlikely(!map->page)) {
            void *page = kzalloc(PAGE_SIZE, GFP_KERNEL);
            /*
             * Free the page if someone raced with us
             * installing it:
             */
            spin_lock_irq(&pidmap_lock);
            if (!map->page) {
                map->page = page;
                page = NULL;
            }
            spin_unlock_irq(&pidmap_lock);
            kfree(page);
            if (unlikely(!map->page))
                break;
        }
        if (likely(atomic_read(&map->nr_free))) {
            for ( ; ; ) {
                if (!test_and_set_bit(offset, map->page)) {
                    atomic_dec(&map->nr_free);
                    set_last_pid(pid_ns, last, pid);
                    return pid;
                }
                offset = find_next_offset(map, offset);
                if (offset >= BITS_PER_PAGE)
                    break;
                pid = mk_pid(pid_ns, map, offset);
                if (pid >= pid_max)
                    break;
            }
        }
        if (map < &pid_ns->pidmap[(pid_max-1)/BITS_PER_PAGE]) {
            ++map;
            offset = 0;
        } else {
            map = &pid_ns->pidmap[0];
            offset = RESERVED_PIDS;
            if (unlikely(last == offset))
                break;
        }
        pid = mk_pid(pid_ns, map, offset);
    }
    return -1;
}

Linux 實現 pid 快速檢索的關鍵,就是通過點陣圖這種數據結構,在系統頁大小為 4K 的情況下,一個頁就可以表示 4096 * 8 = 32768 個 ID,這個數據剛好是《[apue] 進程式控制制那些事兒 》中實測的最大進程 ID 值,看起來 Linux 只用一個記憶體頁就解決了 pid 的快速檢索、分配、釋放等問題,兼顧了性能與準確性,不得不說確實精妙。

pid 範圍

繼續進行之前,先確定幾個常量的值:

/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT	12
#define PAGE_SIZE	(_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK	(~(PAGE_SIZE-1))
    
/*
 * This controls the default maximum pid allocated to a process
 */
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)

/*
 * A maximum of 4 million PIDs should be enough for a while.
 * [NOTE: PID/TIDs are limited to 2^29 ~= 500+ million, see futex.h.]
 */
#define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \
	(sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT))
    
/*
 * Define a minimum number of pids per cpu.  Heuristically based
 * on original pid max of 32k for 32 cpus.  Also, increase the
 * minimum settable value for pid_max on the running system based
 * on similar defaults.  See kernel/pid.c:pidmap_init() for details.
 */
#define PIDS_PER_CPU_DEFAULT    1024
#define PIDS_PER_CPU_MIN    8


#define BITS_PER_PAGE		(PAGE_SIZE * 8)
#define BITS_PER_PAGE_MASK	(BITS_PER_PAGE-1) 
#define PIDMAP_ENTRIES		((PID_MAX_LIMIT+BITS_PER_PAGE-1)/BITS_PER_PAGE) 
    
int pid_max = PID_MAX_DEFAULT;

#define RESERVED_PIDS		300

int pid_max_min = RESERVED_PIDS + 1;
int pid_max_max = PID_MAX_LIMIT;

它們受頁大小、系統位數、CONFIG_BASE_SMALL 巨集的影響,巨集僅用於記憶體受限系統,可以理解為總為 0。列表看下 4K、8K 頁大小與 32 位、64 位系統場景下各個常量的取值:

  PAGE_SIZE BITS_PER_PAGE PID_MAX_DEFAULT PID_MAX_LIMIT PIDMAP_ENTRIES (實際占用)
32 位 4K 頁面 4096 32768 32768 32768 1
64 位 4K 頁面 4096 32768 32768 4194304 128
64 位 8K 頁面 8192 65536 32768 4194304 64

結論:

  • 32 位系統 pid 上限為 32768
  • 64 位系統 pid 上限為 4194304 (400 W+)
  • 32 位系統只需要 1 個頁面就可以存儲所有 pid
  • 64 位系統需要 128 個頁面存儲所有 pid,不過具體使用幾個頁面視 PAGE_SIZE 大小而定

搜索 pid_max 全局變數的引用,發現還有下麵的邏輯:

void __init pidmap_init(void)
{
    /* Veryify no one has done anything silly */
    BUILD_BUG_ON(PID_MAX_LIMIT >= PIDNS_HASH_ADDING);

    /* bump default and minimum pid_max based on number of cpus */
    pid_max = min(pid_max_max, max_t(int, pid_max,
                PIDS_PER_CPU_DEFAULT * num_possible_cpus()));
    pid_max_min = max_t(int, pid_max_min,
                PIDS_PER_CPU_MIN * num_possible_cpus());
    pr_info("pid_max: default: %u minimum: %u\n", pid_max, pid_max_min);

    init_pid_ns.pidmap[0].page = kzalloc(PAGE_SIZE, GFP_KERNEL);
    /* Reserve PID 0. We never call free_pidmap(0) */
    set_bit(0, init_pid_ns.pidmap[0].page);
    atomic_dec(&init_pid_ns.pidmap[0].nr_free);
    init_pid_ns.nr_hashed = PIDNS_HASH_ADDING;

    init_pid_ns.pid_cachep = KMEM_CACHE(pid,
            SLAB_HWCACHE_ALIGN | SLAB_PANIC);
}

重點看 pid_max & pid_max_min,它們會受系統 CPU 核數影響,對於我測試機:

> uname -p
x86_64
> getconf PAGE_SIZE
4096
> cat /proc/cpuinfo | grep 'processor' | wc -l
2
> cat /proc/cpuinfo | grep 'cpu cores' | wc -l
2

為 64 位系統,頁大小 4K,共有 2 * 2 = 4 個核,PID_MAX_LIMIT = 4194304、PID_MAX_DEFAULT = 32768、pid_max_cores (按核數計算的 PID_MAX 上限) 為 1024 * 4 = 4096、pid_min_cores (按核數計算的 PID_MAX 下限) 為 8 *4= 32;初始化時 pid_max = 32768、pid_max_max = 4194304、pid_max_min = 301;經過 pidmap_init 後,pid_max 被設置為 min (pid_max_max, max (pid_max, pid_max_cores)) = 32768、pid_max_min 被設置為 max (pid_max_min, pid_min_cores) = 301。

這裡有一行 pr_info 列印了最終的 pid_max & pid_max_min 的值,通過 dmesg 查看:

> dmesg | grep pid_max
[    0.621979] pid_max: default: 32768 minimum: 301

與預期相符。

CPU 核數超過多少時會影響 pid_max 上限?簡單計算一下: 32768 / 1024 = 32。當總核數超過 32 時,pid_max 的上限才會超過 32768;CPU 核數超過多少時會影響 pid_max 下限?301 / 4 = 75,當總核數超過 75 時,pid_max 的下限才會超過 301。下表列出了 64 位系統 4K 頁面不同核數對應的 pid max 的上下限值:

  pid_max_cores pid_min_cores pid_max pid_max_min PIDMAP_ENTRIES (實際占用)
32 核 32768 128 32768 301 1
64 核 65536 256 65536 301 2
128 核 131072 512 131072 512 4

可見雖然 pid_max 能到 400W+,實際根據核數計算的話沒有那麼多,pidmap 數組僅占用個位數的槽位。

另外 pid_max 也可以通過 proc 文件系統調整:

> su
Password:
$ echo 131072 > /proc/sys/kernel/pid_max
$ cat /proc/sys/kernel/pid_max
131072
$ suspend

[1]+  Stopped                 su
> ./pid
...
[20004] child running
wait child 20004 return 0
duplicated pid find: 20004, total 129344, elapse 74

經過測試,未調整前使用測試程式僅能遍歷 31930 個 pid,調整到 131072 後可以遍歷 129344 個 pid,看來是實時生效了。

搜索相關的代碼,發現在 kernel/sysctl.c 中有如下邏輯:

static struct ctl_table kern_table[] = {
    ...
    {
        .procname= "pid_max",
        .data= &pid_max,
        .maxlen= sizeof (int),
        .mode= 0644,
        .proc_handler= proc_dointvec_minmax,
        .extra1= &pid_max_min,
        .extra2= &pid_max_max,
    },
    ...
    { }
};

看起來 proc 文件系統是搭建在 ctl_table 數組之上,後者直接包含了要被修改的全局變數地址,實現"實時"修改。而且,ctl_table 還通過 pid_max_min & pid_max_max 的值標識了修改的範圍,如果輸入超出了範圍將返回錯誤:

$ echo 300 > /proc/sys/kernel/pid_max
bash: echo: write error: Invalid argument
$ echo 4194305 > /proc/sys/kernel/pid_max
bash: echo: write error: Invalid argument

可以實時修改 pid_max  的另外一個原因還與 PIDMAP_ENTRIES 有關,詳情見下節。

最後補充一點,pidmap_init 是在 start_kernel 中調用的,後者又被 BIOS setup 程式所調用,整體調用鏈是這樣:

boot/head.S -> start_kernel -> pidmap_init

start_kernel 中就是一堆 xxx_init 初始化調用:

查看代碼
 asmlinkage void __init start_kernel(void)
{
    char * command_line;
    extern const struct kernel_param __start___param[], __stop___param[];

    /*
     * Need to run as early as possible, to initialize the
     * lockdep hash:
     */
    lockdep_init();
    smp_setup_processor_id();
    debug_objects_early_init();

    /*
     * Set up the the initial canary ASAP:
     */
    boot_init_stack_canary();

    cgroup_init_early();

    local_irq_disable();
    early_boot_irqs_disabled = true;

    /*
     * Interrupts are still disabled. Do necessary setups, then
     * enable them
     */
    boot_cpu_init();
    page_address_init();
    pr_notice("%s", linux_banner);
    setup_arch(&command_line);
    mm_init_owner(&init_mm, &init_task);
    mm_init_cpumask(&init_mm);
    setup_command_line(command_line);
    setup_nr_cpu_ids();
    setup_per_cpu_areas();
    smp_prepare_boot_cpu();/* arch-specific boot-cpu hooks */

    build_all_zonelists(NULL, NULL);
    page_alloc_init();

    pr_notice("Kernel command line: %s\n", boot_command_line);
    parse_early_param();
    parse_args("Booting kernel", static_command_line, __start___param,
            __stop___param - __start___param,
            -1, -1, &unknown_bootoption);

    jump_label_init();

    /*
     * These use large bootmem allocations and must precede
     * kmem_cache_init()
     */
    setup_log_buf(0);
    pidhash_init();
    vfs_caches_init_early();
    sort_main_extable();
    trap_init();
    mm_init();

    /*
     * Set up the scheduler prior starting any interrupts (such as the
     * timer interrupt). Full topology setup happens at smp_init()
     * time - but meanwhile we still have a functioning scheduler.
     */
    sched_init();
    /*
     * Disable preemption - early bootup scheduling is extremely
     * fragile until we cpu_idle() for the first time.
     */
    preempt_disable();
    if (WARN(!irqs_disabled(), "Interrupts were enabled *very* early, fixing it\n"))
        local_irq_disable();
    idr_init_cache();
    perf_event_init();
    rcu_init();
    tick_nohz_init();
    radix_tree_init();
    /* init some links before init_ISA_irqs() */
    early_irq_init();
    init_IRQ();
    tick_init();
    init_timers();
    hrtimers_init();
    softirq_init();
    timekeeping_init();
    time_init();
    profile_init();
    call_function_init();
    WARN(!irqs_disabled(), "Interrupts were enabled early\n");
    early_boot_irqs_disabled = false;
    local_irq_enable();

    kmem_cache_init_late();

    /*
     * HACK ALERT! This is early. We're enabling the console before
     * we've done PCI setups etc, and console_init() must be aware of
     * this. But we do want output early, in case something goes wrong.
     */
    console_init();
    if (panic_later)
        panic(panic_later, panic_param);

    lockdep_info();

    /*
     * Need to run this when irqs are enabled, because it wants
     * to self-test [hard/soft]-irqs on/off lock inversion bugs
     * too:
     */
    locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD
    if (initrd_start && !initrd_below_start_ok &&
            page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
        pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
                page_to_pfn(virt_to_page((void *)initrd_start)),
                min_low_pfn);
        initrd_start = 0;
    }
#endif
    page_cgroup_init();
    debug_objects_mem_init();
    kmemleak_init();
    setup_per_cpu_pageset();
    numa_policy_init();
    if (late_time_init)
        late_time_init();
    sched_clock_init();
    calibrate_delay();
    pidmap_init();
    anon_vma_init();
#ifdef CONFIG_X86
    if (efi_enabled(EFI_RUNTIME_SERVICES))
        efi_enter_virtual_mode();
#endif
    thread_info_cache_init();
    cred_init();
    fork_init(totalram_pages);
    proc_caches_init();
    buffer_init();
    key_init();
    security_init();
    dbg_late_init();
    vfs_caches_init(totalram_pages);
    signals_init();
    /* rootfs populating might need page-writeback */
    page_writeback_init();
#ifdef CONFIG_PROC_FS
    proc_root_init();
#endif
    cgroup_init();
    cpuset_init();
    taskstats_init_early();
    delayacct_init();

    check_bugs();

    acpi_early_init(); /* before LAPIC and SMP init */
    sfi_init_late();

    if (efi_enabled(EFI_RUNTIME_SERVICES)) {
        efi_late_init();
        efi_free_boot_services();
    }

    ftrace_init();

    /* Do the rest non-__init'ed, we're now alive */
    rest_init();
}

類似 Linux 0.11 中的 main。

pid 分配

先看看 pid 在 Linux 中是如何存放的:

struct pidmap {
    atomic_t nr_free;
    void *page;
};

struct pid_namespace {
    ...
    struct pidmap pidmap[PIDMAP_ENTRIES];
    int last_pid;
    ...
};

做個簡單說明:

  • pidmap.page 指向分配的記憶體頁
  • pidmap.nr_free 表示空閑的 pid 數量,如果為零就表示分配滿了,不必浪費時間檢索
  • pid_namespace.pidmap 數組用於存儲多個 pidmap,數組大小是固定的,以 64 位 4K 頁面計算是 128;實際並不分配這麼多,與上一節中的 pid_max 有關,並且是在分配 pid 時才分配相關的頁面,屬於懶載入策略,這也是上一節可以實時修改 pid_max 值的原因之一
  • pid_namespace.last_pid 用於記錄上次分配位置,方便下次繼續檢索空閑 pid

下麵進入代碼。

初始化

    pid = last + 1;
    if (pid >= pid_max)
        pid = RESERVED_PIDS;
    offset = pid & BITS_PER_PAGE_MASK;
    map = &pid_ns->pidmap[pid/BITS_PER_PAGE];

函數開頭,已經完成了下麵的工作:

  • 將起始檢索位置設置為 last 的下個位置、達到最大位置時回捲 (pid)
  • 確定起始 pid 所在頁面 (map)
  • 確定起始 pid 所在頁中的位偏移 (offset)

這裡簡單補充一點點陣圖的相關操作:

  • pid / BITS_PER_PAGE:獲取 bit 所在點陣圖的索引,對於測試機這裡總為 0 (只分配一個記憶體頁);
  • pid & BITS_PER_PAGE_MAX:獲取 bit 在點陣圖內部偏移,與操作相當於取餘,而性能更好

經過處理,可使用 pid_ns->pidmap[map].page[offset] 定位這個 pid (註:page[offset] 是種形象的寫法,表示頁面中第 N 位,實際需要使用位操作巨集)。

遍歷頁面

    max_scan = DIV_ROUND_UP(pid_max, BITS_PER_PAGE) - !offset;
    for (i = 0; i <= max_scan; ++i) {
        if (unlikely(!map->page)) {
            ...
        }
        if (likely(atomic_read(&map->nr_free))) {
            ...
        }
        if (map < &pid_ns->pidmap[(pid_max-1)/BITS_PER_PAGE]) {
            ++map;
            offset = 0;
        } else {
            map = &pid_ns->pidmap[0];
            offset = RESERVED_PIDS;
            if (unlikely(last == offset))
                break;
        }
        pid = mk_pid(pid_ns, map, offset);
    }

做個簡單說明:

  • 外層 for 迴圈用來遍歷 pidmap 數組,對於測試機遍歷次數 max_scan == 1,會遍歷兩遍
    • 第一遍是 (last_pid, max_pid)
    • 第二遍是 (RESERVED_PIDS, last_pid]
    • 保證即使 last_pid 位於頁面中間,也能完整的遍歷整個 bitmap
  • 第一個 if 用於首次訪問時分配記憶體頁
  • 第二個 if 用於當前 pidmap 內搜索空閑 pid
  • 第三個 if 用於判斷是否遍歷到 pidmap 數組末尾。註意 map 是個 pidmap 指針,所以需要對比地址;(pid_max-1)/BITS_PER_PAGE 就是最後一個有效 pidmap 的索引
    • 若未超過末尾,遞增 map 指向下一個 pidmap,重置 offset 為 0
    • 若超過末尾,回捲 map 指向第一個 pidmap,offset 設置為 RESERVED_PIDS
      • 若回捲後到了之前遍歷的位置 (last),說明所有 pid 均已耗盡,退出外層 for 迴圈
  • 根據新的位置生成 pid 繼續上面的嘗試

對於回捲後 offset = RESERVED_PIDS 有個疑問——是否設置為 pid_max_min 更為合理?否則打破了之前設置 pid_max_min 的努力,特別是當 CPU 核數大於 75 時,pid_max_min 是有可能超過 300 的。

列表考察下“不同的頁面數” & “pid 是否位於頁面第一個位置” (offset == 0) 對於多次遍歷的影響:

PIDMAP_ENTRIES (實際占用) pidmax offset max_scan 遍歷次數 example
1 32768 0 0 1 0 - - -
>0 1 2 0-rear,0-front - - -
2 65536 0 1 2 0,1 1,0 - -
>0 2 3 0-rear,1,0-front 1-rear,0,1-front - -
4 131072 0 3 4 0,1,2,3 1,2,3,0 2,3,0,1 3,0,1,2
>0 4 5 0-rear,1,2,3,0-front 1-rear,2.3,0,1-front 2-rear,3,0,1,2-front 3-rear,0,1,2,3-front

表中根據頁面數和 offset 推算出了 max_scan 的值,從而得到遍歷次數,example 列每一子列都是一個獨立的用例,其中:N-rear 表示第 N 頁的後半部分,N-front 表示前半部分,不帶尾碼的就是整頁遍歷。逗號分隔的數字表示一個可能的頁面遍歷順序。

從表中可以觀察到,當 offset == 0 時,整個頁面是從頭到尾遍歷的,不需要多一次遍歷;而當 offset > 0 時,頁面是從中間開始遍歷的,需要多一次遍歷。這就是代碼 - !offset 蘊藏的奧妙:當 offset == 0 時會減去一次多餘的遍歷!

下麵考察下第一次進入的場景 (以測試機為例):

/*
 * PID-map pages start out as NULL, they get allocated upon
 * first use and are never deallocated. This way a low pid_max
 * value does not cause lots of bitmaps to be allocated, but
 * the scheme scales to up to 4 million PIDs, runtime.
 */
struct pid_namespace init_pid_ns = {
    .kref = {
        .refcount       = ATOMIC_INIT(2),
    },
    .pidmap = {
        [ 0 ... PIDMAP_ENTRIES-1] = { ATOMIC_INIT(BITS_PER_PAGE), NULL }
    },
    .last_pid = 0,
    .level = 0,
    .child_reaper = &init_task,
    .user_ns = &init_user_ns,
    .proc_inum = PROC_PID_INIT_INO,
};
EXPORT_SYMBOL_GPL(init_pid_ns);

last_pid 初始化為 0,所以初始 pid = 1,offset != 0,遍歷次數為 2。不過因為是首次分配,找到第一個空閑的 pid 就會返回,不會真正遍歷 2 次。這裡我有個疑惑:空閑的 pid 會返回 < RESERVED_PIDS 的值嗎?這與觀察到的現象不符,看起來有什麼地方設置了 last_pid,使其從 RESERVED_PIDS 開始,不過搜索整個庫也沒有找到與 RESERVED_PIDS、pid_max_min、last_pid 相關的代碼,暫時存疑。

再考察運行中的情況,offset > 0,遍歷次數仍然為 2,會先遍歷後半部分,如沒有找到空閑 pid,設置 offset = RESERVED_PIDS、同頁面再進行第 2 次遍歷,此時遍歷前半部分,符合預期。

多頁面的情況與此類似,就不再推理了。

頁面分配

        if (unlikely(!map->page)) {
            void *page = kzalloc(PAGE_SIZE, GFP_KERNEL);
            /*
             * Free the page if someone raced with us
             * installing it:
             */
            spin_lock_irq(&pidmap_lock);
            if (!map->page) {
                map->page = page;
                page = NULL;
            }
            spin_unlock_irq(&pidmap_lock);
            kfree(page);
            if (unlikely(!map->page))
                break;
        }

之前講過,頁面採用懶載入策略,所以每次進來得先判斷下記憶體頁是否分配,如果未分配,調用 kzalloc 進行分配,註意在設置 map->page 時使用了自旋鎖保證多線程安全性。若分配頁面成功但設置失敗,釋放記憶體頁面,直接使用別人分配好的頁面;若頁面分配失敗,則直接中斷外層 for 迴圈、失敗退出。

頁內遍歷

        if (likely(atomic_read(&map->nr_free))) {
            for ( ; ; ) {
                if (!test_and_set_bit(offset, map->page)) {
                    atomic_dec(&map->nr_free);
                    set_last_pid(pid_ns, last, pid);
                    return pid;
                }
                offset = find_next_offset(map, offset);
                if (offset >= BITS_PER_PAGE)
                    break;
                pid = mk_pid(pid_ns, map, offset);
                if (pid >= pid_max)
                    break;
            }
        }

檢查 map->nr_free 欄位,若大於 0 表示還有空閑 pid,進入頁面查找,否則跳過。第一次分配頁面時會將內容全部設置為 0,但 nr_free 是在另外的地方初始化的:

    .pidmap = {
        [ 0 ... PIDMAP_ENTRIES-1] = { ATOMIC_INIT(BITS_PER_PAGE), NULL }
    },

它將被設置為 BITS_PER_PAGE,對於 4K 頁面就是 32768。接下來通過兩個巨集進行空閑位查找:test_and_set_bit & find_next_offset,前者是一個位操作巨集,後者也差不多:

#define find_next_offset(map, off)					\
		find_next_zero_bit((map)->page, BITS_PER_PAGE, off)

委托給 find_next_zero_bit,這個位操作函數。定義位於彙編語言中,太過底層沒有貼上來,不過看名稱應該能猜個七七八八。因為是整數位操作,可以使用一些類似 atomic 的手段保證多線程安全,所以這裡沒有施加額外的鎖,例如對於 test_and_set_bit 來說,返回 0 就是設置成功,那就能保證同一時間沒有其它線程在設置同一個比特位,是線程安全的;反之,返回 1 表示已有其它線程占了這個坑,咱們就只能繼續“負重前行”了~

對於占坑成功的線程,atomic_dec 減少空閑 nr_free 數,註意在占坑和減少計數之間還是有其它線程插進來的可能,這會導致插入線程以為有坑位實際上沒有,從而白遍歷一遍。不過這樣做不會產生錯誤結果,且這個間隔也比較短,插進來的機率並不高,可以容忍。

在返回新 pid 之前記得更新 pid_namespace.last_pid:

/*
 * We might be racing with someone else trying to set pid_ns->last_pid
 * at the pid allocation time (there's also a sysctl for this, but racing
 * with this one is OK, see comment in kernel/pid_namespace.c about it).
 * We want the winner to have the "later" value, because if the
 * "earlier" value prevails, then a pid may get reused immediately.
 *
 * Since pids rollover, it is not sufficient to just pick the bigger
 * value.  We have to consider where we started counting from.
 *
 * 'base' is the value of pid_ns->last_pid that we observed when
 * we started looking for a pid.
 *
 * 'pid' is the pid that we eventually found.
 */
static void set_last_pid(struct pid_namespace *pid_ns, int base, int pid)
{
    int prev;
    int last_write = base;
    do {
        prev = last_write;
        last_write = cmpxchg(&pid_ns->last_pid, prev, pid);
    } while ((prev != last_write) && (pid_before(base, last_write, pid)));
}

/*
 * If we started walking pids at 'base', is 'a' seen before 'b'?
 */
static int pid_before(int base, int a, int b)
{
    /*
     * This is the same as saying
     *
     * (a - base + MAXUINT) % MAXUINT < (b - base + MAXUINT) % MAXUINT
     * and that mapping orders 'a' and 'b' with respect to 'base'.
     */
    return (unsigned)(a - base) < (unsigned)(b - base);
}

更新也得考慮線程競爭的問題:這裡在判斷 compare_exchange 的返回值之外,還判斷了新的 last_pid (last_write) 和給定的 pid 參數哪個距離原 last_pid (base) 更遠,只設置更遠的那個,從而保證在競爭後,last_pid 能反應更真實的情況。

內層 for 是無窮迴圈且 offset 單調增長,需要一個結束條件,這就是 offset > BITS_PER_PAGE;另外一個條件是pid >= pid_max,這個主要用於 max_pid 不是整數頁面的情況,例如 43 個 CPU 核對應的 pid_max = 44032,占用 2 個記憶體頁且第二頁並不完整 (44032 - 32768 = 11264,< 32768),此時就需要通過 pid 來終止內層遍歷了。為此需要根據最新 offset 更新當前遍歷的 pid:

static inline int mk_pid(struct pid_namespace *pid_ns,
        struct pidmap *map, int off)
{
    return (map - pid_ns->pidmap)*BITS_PER_PAGE + off;
}

細心的讀者可能發現了,對於 pid 位於頁面中間的場景,回捲後第二次遍歷該頁面時,仍然是從頭遍歷到尾,沒有在中間提前結束 (last_pid),多遍歷了 N-rear 這部分。

對於這一點,我是這樣理解的:這一點點浪費其實微不足道,多寫幾個 if 判斷節約的 CPU 時間可能還補償不了指令流水被打斷造成的性能損失。

pid 釋放

進程結束時釋放 pid,由於之前說過的原因,Linux 支持容器需要對 pid 進行 namespace 隔離,導致這一塊前期的邏輯有點偏離主題 (且沒太看懂),就看看具體的 pid 釋放過程得了:

static void free_pidmap(struct upid *upid)
{
    int nr = upid->nr;
    struct pidmap *map = upid->ns->pidmap + nr / BITS_PER_PAGE;
    int offset = nr & BITS_PER_PAGE_MASK;

    clear_bit(offset, map->page);
    atomic_inc(&map->nr_free);
}

還是經典的 nr / BITS_PER_PAGE 確認頁面索引、nr & BITS_PER_PAGE_MASK 確認 pid 所在比特位偏移;一個 clear_bit 優雅的將比特位清零;一個 atomic_inc 優雅的增加頁面剩餘空閑 pid 數。簡潔明瞭,毋庸多言。

內核小知識

第一次看內核源碼,發現有很多有趣的東西,下麵一一說明。

likely & unlikely

很多 if 條件中都有這個,不清楚是乾什麼的,翻來定義看一看:

# ifndef likely
#  define likely(x)	(__builtin_expect(!!(x), 1))
# endif
# ifndef unlikely
#  define unlikely(x)	(__builtin_expect(!!(x), 0))
# endif

條件 x 使用 !! 處理後將由整數變為 0 或 1,然後傳遞給 __builtin_expect,likely 第二個參數為 1,unlikely 為 0。經過一翻 google,這個是編譯器 (gcc) 提供的分支預測優化函數:

long __builtin_expect(long exp, long c);

第一個參數是條件;第二個是期望值,必需是編譯期常量;函數返回值為 exp 參數。GCC v2.96 引入,用來幫助編譯器生成彙編代碼,如果期望值為 1,編譯器將條件失敗放在 jmp 語句;如果期望值為 0,編譯器將條件成功放在 jmp 語句。實現更小概率的指令跳轉,這樣做的目的是提升 CPU 指令流水成功率,從而提升性能。

        if (unlikely(!map->page)) {
            void *page = kzalloc(PAGE_SIZE, GFP_KERNEL);
            /*
             * Free the page if someone raced with us
             * installing it:
             */
            spin_lock_irq(&pidmap_lock);
            if (!map->page) {
                map->page = page;
                page = NULL;
            }
            spin_unlock_irq(&pidmap_lock);
            kfree(page);
            if (unlikely(!map->page))
                break;
        }
        if (likely(atomic_read(&map->nr_free))) {
            for ( ; ; ) {
                if (!test_and_set_bit(offset, map->page)) {
                    atomic_dec(&map->nr_free);
                    set_last_pid(pid_ns, last, pid);
                    return pid;
                }
                offset = find_next_offset(map, offset);
                if (offset >= BITS_PER_PAGE)
                    break;
                pid = mk_pid(pid_ns, map, offset);
                if (pid >= pid_max)
                    break;
            }
        }

以頁面分配和頁內遍歷為例,這裡有 1 個 likely 和 2 個 unlikely,分別說明:

  • 第一個 unlikely 用來判斷頁面是否為空,除第一次進入外,其它情況下此頁面都是已分配狀態,所以 !map->page 傾向於 0,這裡使用 unlikely;
  • 第二個 unlikely 用來判斷頁面是否分配失敗,正常情況下 !map->page 傾向於 0,這裡使用 unlikely;
  • 第三個 likely 用來判斷頁面是否已分配完畢,正常情況下 atomic_read(&map->nr_free) 結果傾向於 > 0,這裡使用 likely。

總結一下,likely & unlikely 並不改變條件結果本身,在判斷是否進入條件時完全可以忽略它們!如果大部分場景進入條件,使用 likely;如果大多數場景不進入條件,使用 unlikely。

為何編譯器不能自己做這個工作?深入想想,代碼只有在執行時才能知道哪些條件經常返回 true,而這已經離開編譯型語言生成機器代碼太遠了,所以需要程式員提前告知編譯器怎麼生成代碼。對於解釋執行的語言,這方面可能稍好一些。

最後,如果程式員也不清楚哪種場景占優,最好就留空什麼也不添加,千萬不要畫蛇添足。

pr_info 輸出

這個是在 pidmap_init 中遇到的,看看定義:

#ifndef pr_fmt
#define pr_fmt(fmt) fmt
#endif

#define pr_emerg(fmt, ...) \
	printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...) \
	printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...) \
	printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...) \
	printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...) \
	printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn pr_warning
#define pr_notice(fmt, ...) \
	printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
	printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
#define pr_cont(fmt, ...) \
	printk(KERN_CONT fmt, ##__VA_ARGS__)

原來就是 printk 的包裝,pr_info 使用的級別是 KERN_INFO。下麵是網上搜到的 printk 分派圖:

打到 console 的是系統初始化時在屏幕輸出的,一閃而過不太容易看,所以這裡是使用基於 /dev/kmsg 的方式,具體點就是直接使用 dmesg:

$ dmesg | grep -C 10 pid_max
[    0.000000] Hierarchical RCU implementation.
[    0.000000]  RCU restricting CPUs from NR_CPUS=5120 to nr_cpu_ids=2.
[    0.000000] NR_IRQS:327936 nr_irqs:440 0
[    0.000000] Console: colour VGA+ 80x25
[    0.000000] console [tty0] enabled
[    0.000000] console [ttyS0] enabled
[    0.000000] allocated 436207616 bytes of page_cgroup
[    0.000000] please try 'cgroup_disable=memory' option if you don't want memory cgroups
[    0.000000] tsc: Detected 2394.374 MHz processor
[    0.620597] Calibrating delay loop (skipped) preset value.. 4788.74 BogoMIPS (lpj=2394374)
[    0.621979] pid_max: default: 32768 minimum: 301
[    0.622732] Security Framework initialized
[    0.623423] SELinux:  Initializing.
[    0.624063] SELinux:  Starting in permissive mode
[    0.624064] Yama: becoming mindful.
[    0.625585] Dentry cache hash table entries: 2097152 (order: 12, 16777216 bytes)
[    0.629691] Inode-cache hash table entries: 1048576 (order: 11, 8388608 bytes)
[    0.632167] Mount-cache hash table entries: 32768 (order: 6, 262144 bytes)
[    0.633123] Mountpoint-cache hash table entries: 32768 (order: 6, 262144 bytes)
[    0.634607] Initializing cgroup subsys memory
[    0.635326] Initializing cgroup subsys devices

也可以直接 cat /dev/kmsg:

$ cat /dev/kmsg | grep -C 10 pid_max
6,144,0,-;Hierarchical RCU implementation.
6,145,0,-;\x09RCU restricting CPUs from NR_CPUS=5120 to nr_cpu_ids=2.
6,146,0,-;NR_IRQS:327936 nr_irqs:440 0
6,147,0,-;Console: colour VGA+ 80x25
6,148,0,-;console [tty0] enabled
6,149,0,-;console [ttyS0] enabled
6,150,0,-;allocated 436207616 bytes of page_cgroup
6,151,0,-;please try 'cgroup_disable=memory' option if you don't want memory cgroups
6,152,0,-;tsc: Detected 2394.374 MHz processor
6,153,620597,-;Calibrating delay loop (skipped) preset value.. 4788.74 BogoMIPS (lpj=2394374)
6,154,621979,-;pid_max: default: 32768 minimum: 301
6,155,622732,-;Security Framework initialized
6,156,623423,-;SELinux:  Initializing.
7,157,624063,-;SELinux:  Starting in permissive mode
6,158,624064,-;Yama: becoming mindful.
6,159,625585,-;Dentry cache hash table entries: 2097152 (order: 12, 16777216 bytes)
6,160,629691,-;Inode-cache hash table entries: 1048576 (order: 11, 8388608 bytes)
6,161,632167,-;Mount-cache hash table entries: 32768 (order: 6, 262144 bytes)
6,162,633123,-;Mountpoint-cache hash table entries: 32768 (order: 6, 262144 bytes)
6,163,634607,-;Initializing cgroup subsys memory
6,164,635326,-;Initializing cgroup subsys devices

這種會 hang 在結尾,需要 Ctrl+C 才能退出。甚至也可以自己寫程式撈取:

/* The glibc interface */
#include <sys/klog.h>

int klogctl(int type, char *bufp, int len);

不過與前兩個不同,它是基於 /proc/kmsg 的,cat 查看這個文件內容通常為空,與 /dev/kmesg 還有一些區別。限於篇幅就不一一介紹了,感興趣的讀者自己 man 查看下吧。

參考

[1]. Linux內核入門-- likely和unlikely

[2]. Linux內核輸出的日誌去哪裡了

[3]. Pid Namespace 詳解

[4]. namaspace之pid namespace

[5]. struct pid & pid_namespace

[6]. 一文看懂Linux進程ID的內核管理

[9]. linux系統pid的最大值研究

[10]. What is CONFIG_BASE_SMALL=0

本文來自博客園,作者:goodcitizen,轉載請註明原文鏈接:https://www.cnblogs.com/goodcitizen/p/18130888/how_linux_allocate_pid


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 「Pygors系列」一句話導讀: MinGW-w64只有編譯器,MSYS2帶著更新環境,WSL2實用性比較高 歷史與淵源 Windows平臺 Linux平臺 二進位相容 WSL2:運行Linux程式 Wine:運行Windows程式 介面相容 CygWin:編譯Linux程式 Winelib:編譯W ...
  • 文章目錄 一、進程 二、線程 三、進程和線程的區別與聯繫 四、一個形象的例子解釋進程和線程的區別 五、進程/線程之間的親緣性 六、協程 一、進程 進程,直觀點說,保存在硬碟上的程式運行以後,會在記憶體空間里形成一個獨立的記憶體體,這個記憶體體有自己獨立的地址空間,有自己的堆,上級掛靠單位是操作系統。操作系 ...
  • 使用Ansible來部署Apache服務是一個很好的選擇,因為它可以自動化部署過程,確保所有的伺服器上都有相同的配置。以下是一個簡單的步驟指南,展示如何使用Ansible來部署Apache服務: 1 安裝ansible 在基於Debian的系統中,你可以使用以下命令來安裝Ansible: sudo ...
  • 設置SSH免密登錄本機主要涉及生成密鑰對、將公鑰複製到本地(或遠程伺服器,如果是雙向免密)以及測試免密登錄等步驟。以下是一個基本的設置流程: 生成密鑰對: 打開終端或命令提示符,並執行以下命令來生成RSA密鑰對:ssh-keygen -t rsa 系統將會提示你指定保存密鑰文件的路徑和文件名。預設情 ...
  • 簡單回顧 在開始 lab3 的學習之前,我們先簡單回顧下 到目前為止,我們的內核能做了什麼: lab1中,我們學習了 PC啟動的過程,看到BIOS將我們編寫的boot loader 載入記憶體,然後通過bootloader 將內核載入記憶體。同時,使用了一個寫死的臨時頁表(entry_pgdir)完成了 ...
  • 要在Nginx中配置允許跨域(Cross-Origin Resource Sharing, CORS),你需要修改Nginx的配置文件(通常是nginx.conf或者某個包含在nginx.conf中的單獨的配置文件)。下麵是一個基本的例子,展示瞭如何在Nginx中設置CORS: 打開你的Nginx配 ...
  • 具體的軟硬體實現點擊 http://mcu-ai.com/ MCU-AI技術網頁_MCU-AI 打鼾是一種普遍的癥狀,嚴重影響睡眠呼吸障礙患者(單純打鼾者)、阻塞性睡眠呼吸暫停(OSA)患者及其床伴的生活質量。研究表明,打鼾可用於OSA的篩查和診斷。因此,從夜間睡眠呼吸音頻中準確檢測打鼾聲一直是最重 ...
  • Linux 是一種自由和開放源代碼的操作系統,它的使用在全球範圍內非常廣泛。在 Linux 中,進程是操作系統中最重要的組成部分之一,它代表了正在運行的程式。瞭解如何查看正在運行的進程是非常重要的,因為它可以幫助你瞭解系統的運行狀態並對其進行管理。今天飛飛將和你分享如何在 Linux 中查看正在運行... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...