linux内核分析与应用 -- 进程与线程(下)

进程管理(二十二)—CFS调度器 进程管理(十八)—CFS调度器 CFS是内核使用的一种调度器或调度类,它主要负责处理三种调度策略:SCHED_NORMAL、SCHED_BATCH和SCHED_IDLE。调度器的核心在挑选下一个运行的进程时有可能会遍历所有的调度类别。实际上系统大多数进程通常都是CFS调度类负责处理的,因此为了优化下一个进程的挑选调度器核心会先判断当前进程是否采用了CFS调度策略,若是,则直接调用CFS代码来挑选下一个进程,若不是或CFS代码未能挑选到一个合适的进程,则会调用各个调度类的挑选函数来寻找一个合适的进程。若CFS 阅读详情

1.3 进程的调度

在一个 CPU 中,同一时刻最多只能支持有限的进程或者线程同时运行(这取决于 CPU 核数量),但是在一个运行的操作系统上往往可以运行很多进程,假如运行的进程占据 CPU 进程时间很长,就有可能让其他进程饿死。为了解决这种问题,操作系统引入了进程调度器来进行进程的切换,目的是轮流让其他进程获取 CPU 资源。

1.3.1 进程调度机制的架构

在每个进程运行完毕时,系统可以进行调度的工作,但是系统不可能总是在进程运行完才调度,不然其他进程估计还没被调度就饿死了。系统还需要一个重要的机制:中断机制,来周期性地触发调度算法进行进程的切换。

Linux 进程的切换是通过 schedule 函数来完成的,其主要逻辑由 _schedule 函数实现:

static void __sched notrace __schedule(bool preempt)
{
    //  阶级1
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq *rq;
    int cpu;
    cpu = smp_processor_id();           //  获取当前 CPU 的 id
    rq = cpu_rq(cpu);
    rcu_note_context_switch();           //  标识当前 CPU 发生任务切换,通过 RCU 更新状态
    prev = rq->curr;
    …
    //阶段2
    switch_count = &prev->nivcsw;
    if (!preempt && prev->state) {
        if (unlikely(signal_pending_state(prev->state, prev))) {
            prev->state = TASK_RUNNING;
        } else {
            deactivate_task(rq, prev, DEQUEUE_SLEEP);
            prev->on_rq = 0;
            if (prev->flags & PF_WQ_WORKER) {
                struct task_struct *to_wakeup;
                to_wakeup = wq_worker_sleeping(prev, cpu);
                if (to_wakeup)
                    try_to_wake_up_local(to_wakeup);
            }
        }
        switch_count = &prev->nvcsw;
    }
    //  阶段3
    if (task_on_rq_queued(prev))
        update_rq_clock(rq);
    //  阶段4
    next = pick_next_task(rq, prev); //  选取下一个将要执行的进程
    clear_tsk_need_resched(prev);
    clear_preempt_need_resched();
    rq->clock_skip_update = 0;

    if (likely(prev != next)) {
        rq->nr_switches++;
        rq->curr = next;
        ++*switch_count;
        …
        //  阶段5
        rq = context_switch(rq, prev, next);  //进行进程上下文切换
        cpu = cpu_of(rq);
    } else {
        lockdep_unpin_lock(&rq->lock);
        raw_spin_unlock_irq(&rq->lock);
    }

    balance_callback(rq);
}

_schedule 执行过程主要分为以下几个阶段:

1)关闭内核抢占,初始化一部分变量。获得当前 CPU 的 ID 号,并赋值给局部变量 CPU。使 rq 指向 CPU 对应的运行队列(runqueue)。标识当前 CPU 发生任务切换,通知 RCU 更新状态,如果当前 CPU 处于 rcu_read_lock 状态,当前进程将会放入 rnp->blkd_tasks 阻塞队列,并呈现在 rnp->gp_tasks 链表中。(关于 RCU 机制,在第2章中介绍)。关闭本地中断,获取所要保护的运行队列(runqueue)的自旋锁(spinlock),为查找可运行进程做准备。

2)检查 prev 的状态。如果不是可运行状态,而且没有在内核态被抢占,就应该从运行队列中删除 prev 进程。但是,如果它是非阻塞挂起信号,而且状态为 TASK_INTER-RUPTIBLE,函数就把该进程的状态设置为 TASK_RUNNING,并将它插入到运行队列。

3)task_on_rq_queued(prev)将 pre 进程插入到运行队列的队尾。

4)pick_next_task 选取下一个将要执行的进程。

5)context_switch(rq,prev,next)进行进程上下文切换。

通过上述步骤可以发现,调度无非就是找一个已有的进程,然后进行上下文切换,并让它执行而已。

注意 

挑选 next 进程的过程相对复杂,分析起来也比较麻烦,限于篇幅和时间有限,暂时不介绍具体挑选的调度算法实现,这里仅介绍 Linux 调度的架构,图1-10是 Linux 的调度架构图。

图1-10 调度的架构图

Linux 调度架构的核心概念如下:

1)rq:可运行的队列,每个 CPU 对应一个,包含自旋锁、进程数量、用于公平调度的 CFS 信息结构、当前正在运行的进程描述符等。实际的进程队列用红黑树来维护(通过 CFS 信息结构来访问)。

2)cfs_rq:cfs 调度的运行队列信息,包含红黑树的根结点、正在运行的进程指针、用于负载均衡的叶子队列等。

3)sched_entity:把需要调度的东西抽象成调度实体,调度实体可以是进程、进程组、用户等。这里包含负载权重值、对应红黑树结点、虚拟运行时 vruntime 等。

4)sched_class:把调度策略(算法)抽象成调度类,包含一组通用的调度操作接口,将接口和实现分离。你可以根据这组接口实现不同的调度算法,使得一个 Linux 调度程序可以有多个不同的调度策略。

1.3.2 进程切换的原理

在挑选完 next 进程之后,就开始准切换到 next 进程。

可以将进程理解为正在利用 CPU 工作的任务。因为在系统中同时运行的进程有很多,CPU 不能仅仅被同一个进程使用,所以,这时候就需要进程切换机制,另外,假如某进程的工作大部分为 I/O 操作,占用 CPU 空跑会导致资源浪费,这样的进程需要主动放弃 CPU。

需要进程切换的场景有以下几种:

  • 该进程分配的 CPU 时间片用完。

  • 该进程主动放弃 CPU(例如 IO 操作)。

  • 某一进程抢占 CPU 获得执行机会。

Linux 并没有使用 x86 CPU 自带的任务切换机制,而是通过手工的方式实现了切换,切换过程通过以下 switch_to 宏来定义:

#define switch_to(prev, next, last)
do {
    unsigned long ebx, ecx, edx, esi, edi;
    asm volatile("pushfl\n\t"                //  步骤1
        "pushl %%ebp\n\t"                    //  步骤2
        "movl %%esp,%[prev_sp]\n\t"          //  步骤3
        "movl %[next_sp],%%esp\n\t"          //  步骤4
        "movl $1f,%[prev_ip]\n\t"            //  步骤5
        "pushl %[next_ip]\n\t"               //  步骤6
        __switch_canary
        "jmp __switch_to\n"                  //  步骤7
        "1:\t"
        "popl %%ebp\n\t"                     //  从栈恢复 EBP
        "popfl\n"                            //  从栈恢复 flags

        //   asm 内嵌汇编的输出参数
        [prev_sp] "=m" (prev->thread.sp),
            [prev_ip] "=m" (prev->thread.ip),
            "=a" (last),
            "=b" (ebx), "=c" (ecx), "=d" (edx),
            "=S" (esi), "=D" (edi)
            __switch_canary_oparam
            //   asm 内嵌汇编的输入参数
            [next_sp]  "m" (next->thread.sp),
            [next_ip]  "m" (next->thread.ip),
            [prev]     "a" (prev),
            [next]     "d" (next)
            __switch_canary_iparam
        "memory");
} while (0)

该切换过程分为以下几个步骤:

1)pushfl 保存 eflags 寄存器中的数据到进程本身的堆栈。

2)保存堆栈指针 ebp 寄存器地址。

3)把堆栈寄存器 esp 的地址保存到 prev->thread.sp 中。

4)把 next->thread.sp 的地址送入到 sp 寄存器中,这个时候其实已经跑在新的 next 进程的上下文中了。

5)把当前的 eip 地址保存到 prev->thread.ip 中。

6)pushfl 把 next->thread.ip 的地址压入到当前堆栈中。

7)通过 jmp_switch_to 指令,不管 _switch_to 做了什么,ret 返回地址之前已经被设置成了 next->thread.ip 的地址,所以将会执行之前在 copy_thread_tls 中设置的 ret_from_fork。

通过这个过程,可以了解到在 Linux 中,我们并没有对 TSS 进行特殊处理,而是每个 CPU 持有唯一一份 TSS,它的作用也仅仅是在权限级做跃迁的时候保存堆栈上下文,可以通过图1-11理解进程切换机制。

注意 

关于 x86 架构 CPU 的任务切换机制,可以参考阅读《Intel 开发手册》,可以从 Intel 官网下载。另外,本人也编写了代码来模拟两个进程切换的过程,供大家参考,便于加深理解:https://github.com/lingqi1818/analysis_linux/tree/master/ch01/test03 关于 asm 内嵌汇编语法可以参考:https://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.html

图1-11 进程切换原理图

1.3.3 调度中的 CPU 亲和度

我们已经知道,进程创建出来后在内核中的数据结构为 task_struct,该结构中有掩码属性 cpus_allowed,这个掩码由 n 位组成,与 CPU 中的每个逻辑核心一一对应。具有4个核的 CPU 可以有4位。假如 CPU 启用了超线程,那么刚才这个 CPU 就有一个8位的掩码,进程可以运行在掩码位设置为1的 CPU 上。

Linux 内核 API 提供了两个系统调用,让用户可以修改位掩码或查看当前的位掩码:

  • sched_setaffinity():用来修改位掩码。

  • sched_getaffinity():用来查看当前的位掩码。

这两个调用实现的仅仅就是修改或者获取 cpus_allowed 的值。

在下次 task 被唤醒的时候,select_task_rq_fair 根据 cpu_allowed 里的掩码来确定将其置于哪个 CPU 的运行队列,一个进程在某一时刻只能存在于一个 CPU 的运行队列里。

在 Nginx 中,就使用了 CPU 亲和度来完成某些场景的工作:

worker_processes     4;
worker_cpu_affinity 0001 0010 0100 1000;

上面这个配置说明了4个工作进程中的每一个和一个 CPU 核挂钩。

worker_processes     2;
worker_cpu_affinity 0101 1010;

上面这个配置则说明了两个工作进程中的每一个和2个核挂钩。

看 Nginx 的实现,核心函数为 ngx_setaffinity:

void
ngx_setaffinity(uint64_t cpu_affinity, ngx_log_t *log)
{
    cpu_set_t   mask;
    ngx_uint_t  i;
…
    CPU_ZERO(&mask);
    i = 0;
    do {
        if (cpu_affinity & 1) {
        CPU_SET(i, &mask);
    }
    i++;
    cpu_affinity >>= 1;
} while (cpu_affinity);
if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) == -1) {
    ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
            "sched_setaffinity() failed");
    }
}

这里主要的操作就是 sched_setaffinity。

再结合 Nginx 文档中的例子和 Nginx 的源码来看:

worker_processes    4;
worker_cpu_affinity 0001 0010 0100 1000;

如果这个内容写入 Nginx 的配置文件中,然后 Nginx 启动或者重新加载配置的时候,若 worker_process 是4,就会启用4个 worker,然后把 worker_cpu_affinity 后面的4个值当作4个 cpu affinity mask,分别调用 ngx_setaffinity,然后就把4个 worker 进程分别绑定到 CPU 0~3上。

1.4 在应用程序中管理进程和线程

在了解了 Linux 对进程和线程的实现之后,我们首要的目的还是要学习如何在实际应用程序开发中使用这些技术,不同的应用程序实现了不同的进程或线程的管理模型,而每一种模型的背后,都体现了作者对业务的理解和场景化的考虑。下面我们介绍两种不同软件的管理模型。

1.4.1 Memcached 线程池模型分析

Memcached 是一款服务器内存管理软件,它主要是由 pthread 创建的用户工作线程池模型来处理主要逻辑的,图1-12是 Memcached 的线程模型图。

图1-12 Memcached 线程模型图

其主要概念如下:

  • mthread 主线程,主要用于监听 socket 事件,并建立连接,然后把连接和相应的事件分发到 cq 连接队列中(每个分线程都拥有一个连接队列)。

  • cthread 分线程,用于从连接队列中获取连接的读写事件,并进行业务逻辑的处理工作。

从 Memcached 的线程池初始化逻辑中我们可以发现,cthread 是个线程池,用户可以指定池子的大小:

void thread_init(int nthreads, struct event_base *main_base) {
    int         i;
    int         power;

    pthread_mutex_init(&cache_lock, NULL);
    pthread_mutex_init(&stats_lock, NULL);

    pthread_mutex_init(&init_lock, NULL);
    pthread_cond_init(&init_cond, NULL);

    pthread_mutex_init(&cqi_freelist_lock, NULL);
    cqi_freelist = NULL;
    …
    dispatcher_thread.base = main_base;
    dispatcher_thread.thread_id = pthread_self();

    …
    //  在设置完 libevent 后,创建线程
    for (i = 0; i < nthreads; i++) {
        create_worker(worker_libevent, &threads[i]);
    }

    //  等待,直到所有线程设置完毕并返回
    pthread_mutex_lock(&init_lock);
    wait_for_thread_registration(nthreads);
    pthread_mutex_unlock(&init_lock);
}

Memcached 在创建工作线程的时候,同样会用 pipe 调用创建管道,用于和主线程之间的通信。

create_worker 函数最终通过 pthread_create 来创建工作线程:

static void create_worker(void *(*func)(void *), void *arg) {
    pthread_t       thread;
    pthread_attr_t  attr;
    int             ret;

    pthread_attr_init(&attr);

    if ((ret = pthread_create(&thread, &attr, func, arg)) != 0) {
        fprintf(stderr, "Can't create thread: %s\n",
            strerror(ret));
        exit(1);
    }
}

该模型假设在业务逻辑繁忙,并且 I/O 开销比较大的情况下,多线程模型能提高系统的吞吐率。但缺点是当多线程同时访问同一数据的时候就存在竞争,需要额外的并发解决开销(比如锁)。另外其实 Memcached 大部分操作都是基于内存的读写,应该速度很快,引入并发反而在竞争中存在效率降低的风险,另外假如系统中线程数量开得太多,那么线程切换的开销也会上升,需要根据实际场景谨慎设置线程池的大小。而 Redis 的作者认为内存的操作速度是很快的,所以实现了单线程的服务器模型,在下一章介绍并发的时候再详细介绍。

1.4.2 Nginx 进程模型分析

刚才介绍的 Memcached 是比较经典的服务器线程池模型,比如老牌静态服务器软件 Apache 就是采用这样的模型,而 Nginx 的作者则对该模型进行了改进。

Nginx 只要创建 CPU 核心数量相等的工作进程,即可满足高并发、高吞吐量的需求,原因是它的每个工作进程都持有一个基于 I/O 多路复用的 epoll 池子(见图1-13),这样每个进程只有在事件被触发的场景下才进行工作,否则就会让出 CPU 进行其他事件的处理,特别是在 upstream 的场景下,工作进程可以悠闲地等待后端数据准备好之后再进行工作,CPU 的利用率也大大提升。

图1-13 Nginx 工作进程模型

在 Nginx 中 master 进程通过 fork 调用派生完子进程后,又通过 socketpair 创建了管道来进行父子进程之间的通信。

通过了解 Memcached 和 Nginx 的线程池和工作进程模型,我们发现有多种选择,既多线程与单线程,线程池模型与工作进程模型,选择哪种模型好?答案不是绝对的,需要根据业务场景具体分析后,找到问题的症结在哪里,才能给出具体的答案。

1.5 处理进程和线程的相关工具

在了解了 Linux 进程和线程的实现后,在具体的开发和运维场景下如何驾驭它们呢?下面我们简单介绍几个工具,在 Linux 下可用于调试、追踪系统调用并进行性能分析。

1.5.1 开发环境调试线程

当使用 gdb 调试 C 程序的时候,比如 Nginx、Nginx 的子进程都是 fork 出来的,所以当开发完并定义模块设置断点调试的时候,默认是无法进入断点的,gdb 提供了调试线程的方法。

跟踪子进程:

(gdb)set follow-fork-mode child

跟踪父进程:

(gdb)set follow-fork-mode parent

设置 gdb 在 fork 时询问跟踪哪一个进程:

(gdb)set follow-fork-mode ask

根据以上方法进行设置之后,我们就可以在相应的线程实现处设置断点并进行跟踪了。

1.5.2 进程崩溃调试方法

在 C 程序崩溃的时候往往会留下 coredump 文件,供我们分析问题到底出在哪里。下面我们用一个 Nginx 崩溃的场景来分析如何调试 coredump 文件。

曾经遇到个问题,因为后端领取奖品的接口存在并发操作,有可能会出现超领的情况,但是在分析请求日志的过程中发现,在 Tomcat 中,出现了2条领取记录,并且已经成功,但是,在 Nginx 中却只有一条记录。感觉很奇怪,困扰了很久。

不过在分析 Nginx 的 error.log 的时候,发现了一些蛛丝马迹:

[alert] 92648#0: worker process 22459 exited on signal 11

原来在被多领取的时候,还发生过 Nginx 的 woker 进程退出的情况。signal 11也就是 SIGSEGV 信号,说明有非法内存访问的情况。

那么,为什么会有这样的问题呢?难道编写的 Nginx 模块中有潜在的 bug?于是在 Nginx 配置中,设置打开 coredump 的功能:

worker_rlimit_core 500m;
working_directory /tmp;

然后,用 gdb 来调试产生的 coredump 文件:

发现问题出在 get_root_domain 函数,但是由于 Nginx 没有 debug 信息,无法获取具体文件和行号,查看 Nginx 官方文档,编译的时候产生 debug 调试信息可以如下操作:“编译器需要使用正确的参数。假如你使用的是 GCC,-g 参数,会在代码编译后加入调试信息,另外,你需要禁用编译器优化,通过使用 -O0 参数,可以让调试器输出容易看懂的信息。”

我们可以重新编译 Nginx:

CFLAGS="-g -O0" ./configure ....

然后,重新执行:

gdb /usr/sbin/nginx core.23161

显示如下:

sysop@api-1:~$ gdb /usr/sbin/nginx core.23176
GNU gdb (GDB) 7.4.1-debian
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://  gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.? Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://  www.gnu.org/software/gdb/bugs/>...
Reading symbols from /usr/sbin/nginx...Reading symbols from /usr/lib/debug/
    usr/sbin/nginx...done.
done.
[New LWP 23176]
Core was generated by `nginx: worker pr'.
Program terminated with signal 11, Segmentation fault.
#0 0x000000000050267c in?get_root_domain?(domain=<error reading variable:
    Cannot access memory at address 0x8>) at src/http/modules/ngx_http_beacon_
        module.c:298

get_root_domain 代码如下:

size_t get_root_domain(u_char **p,ngx_str_t *domain){
*p = domain->data;
int i = domain->len - 1;
ngx_flag_t is_first = 0;
...

我们发现 domain 指针指向的是0x8这个地址,这么低的地址理论上应该是系统保护的地址,不能被程序访问,那么 domain 是如何来的呢?

domain_len = get_root_domain(&domain,&r->headers_in.host->value);

返现直接取的是 HTTP 协议头中的 host 信息。

然后再看了一下当时的错误信息,有一些获取验证码的请求是用 httpclient 构建的,不是通过浏览器发起的请求,那么 host 信息必然是空的。所以导致这个空指针的异常。

而在当时 Nginx 的进程退出,正好影响了正常的请求,导致返回的时候没打印日志以及吐数据给客户就挂了。

最后程序对这种情况做了兼容,修复了这个诡异的问题。

1.5.3 strace 工具

strace 是 Linux 提供的一个工具,常用来跟踪进程执行时的系统调用和所接收的信号。比如:

[root@lingqi1818 ~]# strace cat /dev/null
execve("/bin/cat", ["cat", "/dev/null"], [/* 27 vars */]) = 0
brk(0)                                  = 0x250d000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
    0x7f1e0c0bf000
access("/etc/ld.so.preload", R_OK)     = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)         = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=26432, ...}) = 0
mmap(NULL, 26432, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1e0c0b8000
close(3)                                   = 0
open("/lib64/libc.so.6", O_RDONLY)         = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\356\1\0\0\0\0\0"...
    , 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1921216, ...}) = 0
mmap(NULL, 3750152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) =
    0x7f1e0bb0d000
mprotect(0x7f1e0bc98000, 2093056, PROT_NONE) = 0
mmap(0x7f1e0be97000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_
    DENYWRITE, 3, 0x18a000) = 0x7f1e0be97000
mmap(0x7f1e0be9c000, 18696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_
    ANONYMOUS, -1, 0) = 0x7f1e0be9c000
close(3)                                   = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
    0x7f1e0c0b7000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
    0x7f1e0c0b6000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
    0x7f1e0c0b5000
arch_prctl(ARCH_SET_FS, 0x7f1e0c0b6700)    = 0
mprotect(0x7f1e0be97000, 16384, PROT_READ) = 0
mprotect(0x7f1e0c0c0000, 4096, PROT_READ)  = 0
munmap(0x7f1e0c0b8000, 26432)              = 0
brk(0)                                     = 0x250d000
brk(0x252e000)                             = 0x252e000
open("/usr/lib/locale/locale-archive", O_RDONLY)        = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=99158720, ...}) = 0
mmap(NULL, 99158720, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1e05c7c000
close(3)                                   = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
open("/dev/null", O_RDONLY)                = 3
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
read(3, "", 32768)                         = 0
close(3)                                   = 0
close(1)                                   = 0
close(2)                                   = 0
exit_group(0)                              = ?

以上代码每一行都是一个系统调用,等号左边是系统调用的函数名及其参数,右边是该调用的返回值。

strace 的具体参数含义可以通过 man 指令来查询,比如我们常用的 -c 参数可以统计每一次系统调用所执行的时间、次数和出错的次数等:

[root@lingqi1818 ~]# strace -c cat /dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
    0.00    0.000000           0         2           read
    0.00    0.000000           0         4           open
    0.00    0.000000           0         6           close
    0.00    0.000000           0         5           fstat
    0.00    0.000000           0         9           mmap
    0.00    0.000000           0         3           mprotect
    0.00    0.000000           0         1           munmap
    0.00    0.000000           0         3           brk
    0.00    0.000000           0         1         1 access
    0.00    0.000000           0         1           execve
    0.00    0.000000           0         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
  100.00    0.000000                    36         1 total

1.5.4 SystemTap 工具

SystemTap 是基于 kprobe 的实现(关于 kprobe 网上资料较多,大家可以自行研究),其功能非常强大,可以监控内核和用户程序。

下面以实际场景为例,来监控运行中程序指定函数的调用参数值。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void print(char *p){
     printf("%s.\n",p);
}

void* test_fn(void* arg)
{
    while(1){
    print("hello pthread");

    sleep(5);  }
    return((void *)0);
}

int main(int argc,char **argv)
{
    pthread_t id;
    int ret;
    ret = pthread_create(&id,NULL,test_fn,NULL);
    if(ret != 0)
    {
        printf("create pthread error!\n");
        exit(1);
    }
    printf("in main process.\n");
    pthread_join(id,NULL);
    return 0;
}

以上程序每5秒钟会调用一次 print,我要是想知道 print 的输入参数是什么,那么编写 SystemTap 脚本如下:

function myprint:string (val)
%{
char *str = (char *)STAP_ARG_val;
snprintf(STAP_RETVALUE, MAXSTRINGLEN, "%s",str);
%}
probe process(3266).function("print") {printf("%s\n",myprint($p));}

运行结果为:

stap –g test.stp

这样就可以获得监控的结果了,因为我用到了内嵌 C 来获取字符串的值,所以就需要加上-g参数。

1.5.5 DTrace 工具

DTrace 是 Oracle 旗下的一款基于 Linux 的监控程序,它可以基于 D 语言编写脚本来实现你想要的监控功能,由于功能比较复杂,这里不做过多阐述,大家可以有兴趣到下面的网址了解更多信息:

下面的脚本用于监控指定 pid 下指定的系统调用是否发生:

test.d:
pid$1::$2:entry
{
    self->trace = 1;
}

    pid$1::$2:return
    /self->trace/
    {
        self->trace = 0;
    }

        pid$1:::entry,
        pid$1:::return
        /self->trace/
        {
        }

然后我们打开 Redis 进程:

localhost:src chenke$ ./redis-server

其 pid 为:

chenke  7276  0.0  0.0  2465080  1892 s003  S+  12:32下午  0:00.01 ./redis-
    server *:6379

现在进行写入操作:

localhost:~ chenke$ telnet localhost 6379
Trying ::1...
Connected to localhost.
Escape character is '^]'.
set a 1
+OK

我们来看监控脚本产生的数据如下:

dtrace: script 'test.d' matched 33829 probes
    CPU     ID                    FUNCTION:NAME
    0       257241                write:entry
    0       257241                write:entry
    0       257241                write:entry
    0       257241                write:entry
    0       257241                write:entry
    0       257241                write:entry

当然,更复杂的功能需要自己研究手册和 D 语言,而且 DTrace 的好处是可以监控用户态的程序。

1.6 本章小结

进程和线程是计算机发展历史上为解决特定问题而产生的解决方案。本章开头介绍了进程管理的历史,以及实现原理,特别是内核线程、用户线程和协程,只有了解了这些原理,才能更好地编写应用程序。

随后介绍了 Linux 对进程和线程的实现,还介绍了内核对进程和线程的调度机制,调度机制的好坏决定了一个操作系统是否能流畅响应不同用户的实时请求。

我们平时使用了很多开源的软件,进程和线程的模型是服务器实现必须要考虑的问题,在了解了原理以及 Linux 的实现后,再对 Memcached 和 Nginx 相关模型进行分析,有助于更好地理解进程和线程。

最后,通过对 gdb、coredump、strace、SystemTap、DTrace 等工具的介绍,有助于我们进行开发调试、故障诊断、监控分析等,建议大家可以多动手实践。

Linux 内核中的线程和进程创建机制 在 Linux 系统中,无论是创建线程还是进程,最终都通过 _do_fork 这一内核函数来完成。用户空间的 fork 和 pthread_create 函数分别调用了 sys_fork 和 sys_clone 系统调用,这些调用在内核中经过层层封装,最终到 _do_fork 函数处理。通过这种统一机制,Linux 内核能够高效地管理进程和线程的创建,并且能够灵活地控制资源的分配与共享。在此机制下,线程与进程的主要区别在于内核处理资源共享的细节,这使得线程更加轻量级,适合用于需要高效并发的应用场景。 阅读详情

相关推荐

微软Media Creation Tool 创建工具 1.3 升级:支持全新下载安装Win11 22H2 Build 22621.525

微软 Media Creation Tool 创建工具 1.3 升级:支持全新下载安装 Win11 22H2 Build 22621.525 ISO 镜像,如果今天运行使用 Media Creation Tool,则会下载 Windows 11 2022 版本 Build 22621.525,它不会向操作系统添加任何新内容,但会修复安装问题,下面来看看详细的更新内容。

zym0218的博客 4230

linux内核源码的个人理解

@linux内核源码的个人理解 SET_USER_NICE 首先,我简单的介绍一下SET_USER_NICE源码的大致功能: 就简单地从字面意思上看,SET:设置、USER:用户、NICE:优先级的值。所以SET_USER_NICE连起来就是设置用户的优先级,也就是设置进程的优先级。 NICE是反应进程优先级的一个值,范围是[-19,+20],一共有40个值,值越小,代表的优先级更高;反之,值越大...

tigahhh的博客 3899

基于车辆运动学的规划算法 - Hybrid A*/ State Lattice Planning (前置知识:自行车模型、Dubins、Reeds Shepp、多项式曲线、螺旋曲线)

本文将按顺序讲解基于车辆运动学的规划算法,以及需要的前置知识:自行车模型、Dubins和Reeds Shepp曲线、多项式曲线和螺旋曲线

BigDavid123的博客 4857

nginx报错---sched_setaffinity

新项目压测发现报错 2017/02/13 20:38:14 [alert] 9391#0: sched_setaffinity(0x0000000000000080) failed (22: Invalid argument) 百度后知道是worker_cpu_affinity指令设置错误导致的问题. worker_cpu_affinity命令是设置Nginx进程与CPU进行绑定的.可以

wolf 1万+

Python爬虫实战:5分钟搞定慢慢买历史价格查询(附完整代码)

本文提供了一份Python爬虫实战指南,教你如何快速编写脚本自动查询慢慢买网站的商品历史价格。文章详细拆解了环境准备、接口分析、核心代码实现及常见问题应对,并附有完整代码,帮助读者将爬虫技术应用于日常比价,实现高效购物决策。

weixin_29053577的博客 1007

PostgreSQL数据库全攻略:从入门到精通

本文是一份PostgreSQL数据库综合指南,涵盖安装配置、基础操作和高级管理。主要内容包括:1) PostgreSQL简介与各平台安装方法;2) 数据库基本操作,如创建数据库/表、CRUD操作、用户权限管理;3) 高级功能包括索引优化、事务控制、视图、存储过程、触发器和扩展模块;4) 数据库管理维护,涉及配置调优、备份恢复、性能监控和日常维护;5) 安全设置与高可用方案。指南提供了从入门到进阶的实用知识,强调结合官方文档进行深入学习,帮助用户高效使用这一功能强大的开源关系型数据库系统。

索大神的博客 1967

解决nginx报sendfile() failed (22: Invalid argument)访问图片css js net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK)

解决nginx报sendfile() failed (22: Invalid argument)访问图片css js net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK)

gusijin的博客 1386

SD--根据订单创建出库单(相关的函数列表的介绍系列篇(2))

创建订单样例代码参见:http://blog.youkuaiyun.com/CompassButton/archive/2007/05/09/1601712.aspx 可以根据一个订单或者部分订单项来创建出库单,代码如下:* Populating the item data lx_sales_item-ref_doc = l_vbeln. lx_sales_item-ref_item = c_000010

SAP BLOG--Jack Wu 6226

Linux调度-主调度器

核心调度器,其实就是schedule这一个函数,当一个进程觉得自己无事可做了,可以调用该函数释放CPU,让其他进程占用CPU完成任务。粗略看下代码,做了一些注释。 asmlinkage __visible void __sched schedule(void) { struct task_struct *tsk = current; sched_submit_work(tsk); do...

crazy_koala的博客 526

机器人关节电机发热难题:从工程化角度拆解热管理解决方案(下)

摘要:本文系统分析了机器人关节电机与驱动系统的热管理方案,重点解决局部热点问题。针对不同功率场景提出了热管/均热板散热(适用于中小功率)和液冷散热(适用于高功率)的具体实施方案,详细比较了结构参数、材料选择、安装工艺和散热效果。特别强调了电机-驱动协同热管理的重要性,提出"工况联动+热路径共享"的核心理念,包括一体化散热结构设计和动态控制策略。最后介绍了热仿真与测试验证方法,确保方案的有效落地。通过系统级热管理,可显著降低温升(热点区域降温30-60℃),同时优化体积和能耗。

bother3000的博客 3325

Android 12.0 以太网和wifi共存的产品wifi优先级设置功能实现

在12.0的系统ROM定制化开发中,在对于一些有线网和wifi共存的产品中,由于某些产品的特殊要求首选使用wifi,而在原生 系统中以太网的优先级高于wifi的,所以就需要从优先级的设置来分析,实现设置优先级的相关功能实现

安卓兼职framework和app工程师的博客 943

linux内核分析和应用 -- 进程与线程(上)

只要是计算机科班出身的技术人员,肯定都学过现代操作系统课程。一般在操作系统的书中都会有这样的定义: 简单来说,进程就是在操作系统中运行的程序,是操作系统资源管理的最小单位。一个进程可以管理多个线程,线程相对轻量,可以共享进程地址空间。 我在很多次面试的时候,向求职者提问过进程和线程在 Linux 中到底有什么区别,不只是科班出身的应届生,连工作多年的老手,也有很多回答不准确。传统的教育缺乏...

~~ LINUX ~~ 1004

PMO与供应链管理的整合:端到端项目交付的优化策略

在当今全球化和市场竞争日益激烈的环境中,组织面临着前所未有的项目复杂性和交付压力。传统上相互独立的项目管理办公室(PMO)与供应链管理(SCM)正逐渐意识到:只有通过无缝整合,才能实现真正高效的端到端项目交付。本文深入探讨了PMO与供应链管理整合的战略价值、核心框架和实施路径,通过生动的比喻、实际案例和实用工具,展示了如何打破部门壁垒,构建协同高效的项目生态系统。无论您是PMO负责人、供应链专业人士还是高级管理层,都将从本文获得重塑项目交付模式、优化资源利用、降低风险并提升组织竞争力的宝贵见解和行动指南。

项目管理的博客 946

linux内核线程之深入浅出,Linux内核线程之深入浅出

http://blog.youkuaiyun.com/sailor_8318/archive/2008/06/04/2509320.aspx【摘要】本文首先介绍了进程和线程的区别,接着分析了内核线程、轻量级LWP线程以及常见的用户线程的特点,同时介绍了内核线程和进程的区别。分析了创建内核线程kernel_thread函数的实现过程,介绍了一个在驱动中使用内核线程的实例。最后针对内核线程创建销毁的特点,给出了通...

weixin_34856035的博客 265

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

linux 多线程多进程如何选择,多线程vs多进程,谁在Linux能更好发挥多核CPU的能力...

系统:SuseLinux9企业版环境:ATCA的Blade2.4G至强双CPU,8G内存处理的内容呼叫(没有硬盘读写,都是网络通信)高手们帮忙解答一下,问题是如题BTW某些同事说,Linux2.6内核中,进程相当于线程还来出来,不知道是不是这样?我以为在Linux中还是线程还是当成简单的进程处理的。|1、Linux内核是以LWP为调度单位的。2、线程分内核线程和用户空间线程。用户空...

weixin_36485139的博客 332

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

全面解析Linux 内核 3.10.x - 内核进程

From: 全面解析Linux 内核 3.10.x - 进程管理 三千大世界,五千小世界,却是由生命组成 - XXXX进程类似于人类,它们被产生,有或多或少的有效生命。可以产生一个或者多个子进程,最终都要走向”死亡”。为什么说进程是操作系统的灵魂?因为它之于操作系统就犹如生命对于自然,犹如人之于家。一、透过现象看本质从用户程序的角度看,进程就是一次运行活动,是程序的执行实体! 从内核的角度看,进程

何文的专栏 2298

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Linux 线程与进程的学习

【摘要】本文首先介绍了进程和线程的区别,接着分析了内核线程、轻量级LWP线程以及常见的用户线程的特点,同时介绍了内核线程和进程的区别。分析了创建内核线程kernel_thread函数的实现过程,介绍了一个在驱动中使用内核线程的实例。最后针对内核线程创建销毁的特点,给出了通用的内核线程操作函数API,使用该API可在自己的驱动或内核代码中方便的使用内核线程。   1      线程和进程的差别

绝影 542

深入浅出linux内核-第四篇-Linux进程原理与系统调用

本文介绍了Linux操作系统中进程管理的核心概念。进程作为操作系统资源管理的基本单元,本质上是程序的动态执行实例,在Linux中被称为任务(task)。文章详细阐述了进程的生命周期状态(创建、就绪、执行、阻塞、终止),重点解析了Linux内核中的进程控制块(task_struct)数据结构,包括进程状态标志、调度优先级、内存管理、文件系统等关键成员。同时探讨了进程优先级分类(限期进程、实时进程、普通进程)及其调度机制,并说明了系统调用和内核线程的工作原理。最后介绍了进程退出的两种方式:正常退出和异常终止。

aizhuyesu31的博客 1417

用户进程与内核进程是一一对应的吗_Linux内核同步,进程,线程同步

前言:本文浅谈两个知识点:1:用户级线程和内核级线程的区别,以及各个优缺点2:linux内核同步,进程,线程同步一:用户级线程和内核级线程的区别(1)内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。(2)用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同...

weixin_39672296的博客 323

进程、线程、内核线程与用户线程

应用并发(普通程序):使用 pthread(用户线程)或更高层并发框架(线程池、协程);高并发 I/O 或短时任务:用户线程 + 异步 I/O / 事件驱动 / 协程通常效率高;内核任务、驱动、需要直接访问内核资源或长期后台任务:使用内核线程。

huayidw的博客 1032

linux下进程和线程

前言 学习自用,这篇还比较没头绪,标题和内容很难对上 问题 子进程能继承父进程的什么东西?不能继承父进程的什么东西? fork以后会生成一个task_struct,还会修改页表吧,每个线程还都有自己的thread_info,在栈的底部(应该是线程栈吧),其中有一个成员指针指向task_struct 信号处理函数不能继承,毕竟在子进程里对应的处理函数的地址是无效的(来自APUE),未决信号集也是不能继承的, 而task_struct存有被阻塞的信号集 线程独立的资源 linux内核并没有.

ptgood的博客 278

进程原理以及系统调用

一、进程\进程生命周期一、进程\进程生命周期1.进程操作系统作为硬件的使用层,提供使用硬件资源的能力,进程作为操作系统使用层,提供使用操作系统抽象出的资源层的能力。进程:是指计算机中已运行的程序。进程本身不是基本的运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。Linux 内核把进程叫做任务(task),进程的虚拟地址空间可分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。

m0_74186706的博客 1110

深入理解Linux内核进程管理与线程机制

值得注意的是,现代Linux系统中实际调度单位是线程,但传统进程概念仍然非常重要。平台提供的一键部署功能特别适合需要持续运行的进程监控、服务端应用等场景,免去了繁琐的环境配置过程。Linux提供了多种IPC机制,包括信号量、共享内存、消息队列、管道、套接字等。每个进程都拥有独立的地址空间,主要包括代码段、数据段、BSS段、堆和栈等部分。POSIX线程(pthread)是Linux中的标准实现,需要注意分离线程与非分离线程的区别,以及线程安全函数的使用规范。互斥量是最基本的同步工具,但使用时需注意死锁风险。

PearlOwl67的博客 745

linux 2.6进程与线程

<br /> 1 >    线程和进程的差别<br /> 线程机制支持并发程序设计技术,在多处理器上能真正保证并行处理。而在linux实现线程很特别,linux把所有的线程都当作线程实现。<br /> linux下线程看起来就像普通进程(只是该进程和其他进程共享资源,如地址空间)。上述机制与Microsoft windows或是Sun Solaris实现<br /> 差异很大。这些系统提供专门支持线程机制(轻量级进程)。<br /> 在现代操作系

hq0520的专栏 528

Linux线程

Linux线程周边概念、线程的控制、pthread库和c++11thread库的使用

qq_33093885的博客 1161

大话进程与线程

一.线程概念 进程和线程的比喻 一个系统运行着很多进程,可以比喻为一条马路上有很多马车 不同的进程可以理解为不同的马车 而同一辆马车可以有很多匹马来拉----这些马就是线程 假设道路的宽度恰好可以通过一辆马车 道路可以认为是临界资源 那么马车成为分配资源的最小单位(进程) 而同一个马车被很多匹马驱动(线程)----即最小的运行单位 每辆马车马匹数>=1 所以马匹数=1的时候进程

kingbaby20lin的专栏 814
上一篇: 一个草根程序员如何进入BAT
下一篇: DPDK学习1 -- DPDK环境搭建和测试helloword
废言Pro
博客等级 码龄16年 1092粉丝 142原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值