分类 Linux 相关 下的文章

关于 Linux poll 的 timeout 参数的解释

最近研究 JDK 和 Netty 里面 socket timeout 的实现, 代码层面从 Java 代码一直追踪到 Linux 上的系统调用 poll. 当查看 poll 的手册的时候, 注意到一个不正常的描述.

首先, 通过 google 找到的是这个手册: https://linux.die.net/man/2/poll. 这个手册里面有关于 poll 的 timeout 参数的描述是:

The timeout argument specifies the minimum number of milliseconds that poll() will block.

注意, 它使用的是 "minimum", 那么对于正数的 timeout 值, 我可以有这些可能的理解:

  1. 不管有没有感兴趣的事件发生, 我最少必须等 timeout 的毫秒数;
  2. 在 timeout 值的时间窗口内, 有感兴趣的事情发生, 就直接返回, 若没有, 最少等 minimum 毫秒数;
    那么, 对于以上不管哪种理解, 最少的等待时间是 timeout 毫秒数, 那么最大的等待时间呢? 如果在 timeout 毫秒数内么有感兴趣的事件发生, 要等多久呢?

怀着这个问题, 我又返回去看 JDK 里的代码:
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/native/java/net/SocketInputStream.c#l101
还是无法理解.

于是重新 google 搜索, 找到了这个问答:
https://stackoverflow.com/questions/529975/what-does-poll-do-with-a-timeout-of-0
pollMan.png
回答里面明显表明这是最大等待时间, 于是重新找另外的 Linux man page:
https://man7.org/linux/man-pages/man2/poll.2.html

The timeout argument specifies the number of milliseconds that poll() should block waiting for a file descriptor to become ready.

这里的意思, 就没有任何歧义了, 就是最大等待时间.

Linux 的 scheduler

Linux 从 2.6.23 开始使用 CFS(Completely Fair Scheduler). 在 Linux 内核看来, 它不区分进程和线程, 每个都是可调度的 Thread.

每个新创建的 Thread 都有一个 Schedule Policy 属性和一个 Static Priority 属性. CFS 主要根据这2 个属性的值, 确定当前线程是不是能分配 CPU 时间.

Linux 上的 Scheduler Policy 主要有:

  1. SCHED_OTHER # Linux kernel 里面又名 SCHED_NORMAL
  2. SCHED_IDLE
  3. SCHED_BATCH
  4. SCHED_FIFO #First In First Out 先进先出
  5. SCHED_RR # Round Robin 是 SCHED_FIFO 的改进版本

以上这些 Policy 又被分为 2 大类(Classes):

  1. Normal scheduling policies (正常) 包含 SCHED_OTHER, SCHED_IDLE, SCHED_BATCH;
  2. real-time policies (实时) 包含 SCHED_FIFO, SCHED_RR

对于 Normal scheduling policies 的线程, 它的静态优先级一定是 0, 这个值在scheduler 安排他们在 CPU 上执行决策的时候, 是不考虑的.
对于 Real-time policies 的线程, 它的静态优先级范围是 1(低) ~ 99(高).

从概念上讲(实际并不一定是这么做的, 效果是一样的), 每个优先级都有一个线程列表, 里面都是同优先级的线程. CFS 从高优先级队列到低优先级队列按照顺序去找, 高优先级队列里面执行完了, 再执行第优先级队列. 同一个队列里面从头往后执行.

不同优先级线程之间根据优先级(priority) 去安排, 相同优先级的线程根据 Scheduler Policy 去安排执行顺序和时间;

Linux 默认的线程 Policy 是SCHED_OTHER, 所以这些默认的静态优先级都是 0. 它们都比 Real-time 的线程优先级低. 对于 SCHED_OTHER 队列里面的线程, 因为静态优先级都是0, 所以它们使用动态优先级(dynamic priority) 来决定谁先运行. 动态优先级使用 nice 的值来决定谁先运行. nice 的值决定一个线程在一个时间区间内可以运行的时长, 通常情况下没有 nice 值的线程都有相同的运行时长, 改变 nice 的值, 就能改变在一个时间区间内它能运行的时长.

nice 的值只对 policy 是 SCHED_OTHER & SCHED_BATCH 的线程有效. 在 Linux 上 nice 的值是 per-thread 的. 在当前的 Linux 系统, nice 的值可以是: -20 (high priority) to +19 (low priority). 它意味着你越 nice, 你的优先级越低, 越不 nice (负值), 你的优先级就越高. 当前 Linux 系统里面, nice 的值相差一个单位, 一个时间区间内大概运行时间比原来是 1:1.25.

PolicyStatic PriorityDynamic Priority(nice)
SCHED_OTHER0-20 ~ 19
SCHED_RR0 ~ 99N/A

我们看到上面说 Linux 的 Real-time 的线程优先级是 1(低) ~ 99(高), 而 Normal 的线程优先级是 0, 根据 nice 可以调整的值, Normal 线程可以做到nice 的值 -20 ~ 19, 如果 mapping 的到优先级, 则可以是 0 ~ 39. 这里 0 ~ 39 值越低, 优先级越高. 如果把 Real-time 的优先级都加一个负号, 就变成了 -99(高) ~ 1(低), 那么就可以连起来了. 所有优先级是: -99 ~ 1, 0 ~ 39, 并且值越小, 优先级越高. 所以可以认为 Normal 的优先级默认是 20(本来是 0), 通过 nice 改成了 0(-20) ~ 39 (19). 这样一来, 是不是就跟 top 里面的 prio 和 ni 值匹配了?

普通用户只能使用 nice 的值把进程调的更 nice (0 ~ 20), 不能调的更不 nice (-10 ~ -1). 但是 root 可以.

另外, 当没有 CPU 竞争的时候, 比如系统有 8 个 CPU, 只有 3 个在忙, Priority 基本不起作用.

参考: https://man7.org/linux/man-pages/man7/sched.7.html

Linux 线程的状态

通常的操作系统概念里, 都会区分进程和线程, 可是在 Linux 内核里面, 对于系统调度来说, 它不区分进程和线程, 统一的术语都是线程(Thread), 线程做为调度的基本单位.

对于一个系统来说, 硬件的处理器(CPU)个数(插槽数), 一个 CPU 上的核(core)的数量, 一个核上的硬件线程数(Hyper-Thread)数都是有限的. 有的线程正在 CPU 上运行(on-cpu), 有的不在 CPU 上运行(off-cpu). 那么对于 Thread 来说, 它有哪些状态呢?

通常我们会在 Linux 上面看到这些状态:
• Runnable state 可运行状态,正在 CPU 运行或在队列等待运行
• Sleeping state 等待 I/O 或 调用 sleep 函数 响应 其它 interrupt
• Uninterruptable sleep state 等待 I/O 不响应其它 interrupt
• Defunct or Zombie state 等待父线程/root 线程 ack 死亡

为什么会有僵尸(Zombie)状态?
每个进程运行结束之后, 会返回一个状态码, 本身结束运行之后, 进入 Zombie 状态, 然后创建它的父进程(除了 1 号进程没有父进程)收到这个状态码, 做个响应, 它就可以正常死亡了. 如果它的父进程一直没时间响应, 它就会一直停留在 Zombie 状态. 如果某个进程的父进程早就自己死亡了, 那么 1 号进程就会作为它的父进程进行响应.

如果在 ps 命令下, 我们能看到稍多状态:

D    uninterruptible sleep (usually IO)
I    Idle kernel thread
R    running or runnable (on run queue)
S    interruptible sleep (waiting for an event to complete)
T    stopped by job control signal
t    stopped by debugger during the tracing
X    dead (should never be seen)
Z    defunct ("zombie") process, terminated but not reaped by its parent

对于 ps 命令, 这些状态从哪里获得?
从 /proc/<pid>/task/<pid>/status 得到, 比如:

supra@suprabox:/proc/3119/task/3125$ cat /proc/3119/task/3125/status
Name:    gmain
Umask:    0002
State:    S (sleeping)
Tgid:    3119

对于 sleep 的线程, 它到底从哪个函数进入的 sleep?
通过 cat /proc/<pid>/task/<pid>/wchan 可获得(wchan: waiting channel), 比如:

cat /proc/3119/task/3125/wchan
poll_schedule_timeout.constprop.13

进入 sleep (S & D) 状态的线程, Scheduler 不会再给他们安排 CPU 执行它们的代码. 当线程进入 sleep 之前, 它会进入内核态, 更新它自己在内核的里面 scheduler 数据结构的状态, 告诉内核scheduler 把自己从等待执行的队列中放到一个等待队列中. 当自己等待的信号(I/O interrupt 或者 sleep的时间到), 内核处理 interrupt 的内核进程会重新把它们放到认为等待执行队列. 于是它们重新获得执行机会.