Linux内核-进程运转方式

进程运转方式

系统时间

Linux 0.11 的系统当前时间计算方式是:

当前 Unix 时间 = 启动时的 Unix 时间 + 开机后的运行秒数
 = startup_time + jiffies / HZ

开机时间获取

CPU内部有一个系统时间RTC(cmos),会在上电的时候调用mktime函数计算出从1970年1月1日0时到当前开机时间经过的秒数

// 根据刚刚的那些时分秒数据,计算从 1970 年 1 月 1 日 0 时起到开机当时经过的秒数,作为开机时间,存储在 startup_time 这个变量里
long kernel_mktime(struct tm * tm)

在main函数中被调用:

//kernel/blk_drv/main.c
// 初始化时间, 从CMOS芯片中获取
static void time_init(void)
{
    struct tm time;

    do {
        time.tm_sec = CMOS_READ(0);             // 电源关闭时CMOS芯片中的RTC(Real Time Clock)由计算机内部的电池供电,保持活动状态
        time.tm_min = CMOS_READ(2);
        time.tm_hour = CMOS_READ(4);
        time.tm_mday = CMOS_READ(7);
        time.tm_mon = CMOS_READ(8);
        time.tm_year = CMOS_READ(9);
    } while (time.tm_sec != CMOS_READ(0));
    BCD_TO_BIN(time.tm_sec);
    BCD_TO_BIN(time.tm_min);
    BCD_TO_BIN(time.tm_hour);
    BCD_TO_BIN(time.tm_mday);
    BCD_TO_BIN(time.tm_mon);
    BCD_TO_BIN(time.tm_year);
    time.tm_mon--;
    startup_time = kernel_mktime(&time);
}

这里先去读取CMOS芯片中的RTC值,并转换UNIX时间辍,这里startup_time获取得到计算出来的时间值,并且会为jiffies所用

time_init()只读取一次 RTC。此后时间向前推进不再反复读取CMOS,而是依赖 jiffies

系统滴答

jiffies是系统的一个时钟滴答,一个系统滴答是10ms,使用的是定时器中断触发

sched_init()函数中初始化定时器

    // 下面代码用于初始化 8253 定时器
    outb_p(0x36,0x43);        /* binary, mode 3, LSB/MSB, ch 0 */
    outb_p(LATCH & 0xff , 0x40);    /* LSB */
    outb(LATCH >> 8 , 0x40);    /* MSB */             //  这四行代码就开启了这个定时器,之后这个定时器变会持续的、以一定频率的向 CPU 发出中断信号, 中断处理程序为 timer_interrupt
    set_intr_gate(0x20,&timer_interrupt);

定时器每10ms触发一次中断,对应代码在kernel/system_call.s

_timer_interrupt:
    push %ds        # save ds,es and put kernel data space
    push %es        # into them. %fs is used by _system_call
    push %fs
    pushl %edx        # we save %eax,%ecx,%edx as gcc doesn't
    pushl %ecx        # save those across function calls. %ebx
    pushl %ebx        # is saved as we use that in ret_sys_call
    pushl %eax
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    movl $0x17,%eax
    mov %ax,%fs
    incl _jiffies   #------------->在这里jiffies每次+1
    movb $0x20,%al        # EOI to interrupt controller #1
    outb %al,$0x20
    movl CS(%esp),%eax
    andl $3,%eax        # %eax is CPL (0 or 3, 0=supervisor)
    pushl %eax
    call _do_timer        # 'do_timer(long CPL)' does everything from
    addl $4,%esp        # task switching to accounting ...
    jmp ret_from_sys_call

系统当前时间

sched.h中有一个CURRENT_TIME宏定义:

#define CURRENT_TIME (startup_time+jiffies/HZ)        // 当前时间(秒数)

这里就是定义了系统当前时间是如何表达的

获取系统时间

用户程序调用time()系统调用后,最终进入获取CURRENT_TIME

//kernel/sys.c
//如果 tloc 不为 null,则时间值也存储在那里
int sys_time(long * tloc)
{
    int i;

    i = CURRENT_TIME;
    if (tloc) {
        verify_area(tloc,4);
        put_fs_long(i,(unsigned long *)tloc);  // 也放入用户数据段 tloc 处
    }
    return i;
}

设置系统时间

在系统启动之后,超级用户调用stime()设置系统时间,最终是

// 设置系统时间和日期。参数 tptr 是从 1970 年 1 月 1 日 00:00:00 GMT 开始计时的时间值(秒)。 
 // 调用进程必须具有超级用户权限。
int sys_stime(long * tptr)
{
    if (!suser())    //super user
        return -EPERM;
    startup_time = get_fs_long((unsigned long *)tptr) - jiffies/HZ;     //开始时间为负值
    return 0;
}

这里是开始时间等于用户设置时间减去jiffies/HZ

为什么要减去 jiffies/HZ?因为当前时间始终按照下面的公式计算:

CURRENT_TIME = startup_time + jiffies/HZ

假设用户想把当前时间设置为 new_time,那么就需要:

startup_time + jiffies/HZ = new_time

移项得到:

startup_time = new_time - jiffies/HZ

所以这里实际修改的也是时间基准 startup_time。

定时器中断函数do_timer

kernel/system_call.s代码中的_timer_interrupt最后调用了call _do_timer函数

// 时钟中断 C 函数处理程序,
// 每个时钟中断都要更新首个任务的定时器,
// 然后进行相应的定时器处理程序,然后进行任务调度
void do_timer(long cpl)      //current priority level 保存的是当前被中断的进程是内核态(0)还是用户态(1)

整体流程:

  时钟中断
      │
      ├─ jiffies++
      │
      ├─ 统计 utime/stime
      │
      ├─ 处理软件定时器
      │
      ├─ current->counter--
      │
      ├─ counter > 0?
      │      │
      │      ├─ 是 → 从中断返回,当前进程继续运行
      │      │
      │      └─ 否 → counter 置 0
      │
      ├─ 中断前处于内核态?
      │      │
      │      ├─ 是 → 暂不抢占,从中断返回
      │      │
      │      └─ 否 → 调用 schedule()
      │
      └─ schedule() 选择 counter 最大的可运行进程

运行时间

这里面有一个逻辑

    if (cpl)
        current->utime++;
    else
        current->stime++;

cpl为1表示用户态,为0表示内核态。为1用户态进程的时间+1.为0内核态的时间+1

这里的current是一个task_struct *指针,指向的是init_task.task的地址

// 任务联合,包括任务结构体和内核堆栈
union task_union {
    struct task_struct task;
    char stack[PAGE_SIZE];
};

// 定义初始任务的数据(sched.h 中) 非常重要!!!!
static union task_union init_task = {INIT_TASK,};
struct task_struct *current = &(init_task.task);        // 当前任务指针

在linux中每个任务都是使用task_struct结构体表示,所以这里current就代表当前任务的指针

utimestime分别代表用户运行时间系统运行时间,它们是用于统计当前进程占用了多少CPU时间,而不是统计现实时间或者系统启动时间过去了多久

所以这里的“系统运行时间”更准确地说是:

▎ 当前进程在内核态消耗的 CPU 时间。

它不是整个 Linux 系统运行了多长时间。整个系统的开机运行时间由全局变量 jiffies 表示。

  ┌──────────────┬─────────────────┬───────────────────┬──────────────────────────┐
  │    计数器    │     属于谁      │   什么时候增加    │           用途           │
  ├──────────────┼─────────────────┼───────────────────┼──────────────────────────┤
  │ jiffies      │ 整个系统,全局  │ 每次时钟中断都增  │ 记录开机时间、定时器、调 │
  │              │ 变量            │ 加                │ 度等                     │
  ├──────────────┼─────────────────┼───────────────────┼──────────────────────────┤
  │ current->uti │ 当前进程        │ 中断前正在用户态  │ 统计进程的用户态 CPU     │
  │ me           │                 │ 运行              │ 时间                     │
  ├──────────────┼─────────────────┼───────────────────┼──────────────────────────┤
  │ current->sti │ 当前进程        │ 中断前正在内核态  │ 统计进程的内核态 CPU     │
  │ me           │                 │ 运行              │ 时间                     │
  └──────────────┴─────────────────┴───────────────────┴──────────────────────────┘

定时器链表

定义定时器链表结构如下:

// 定时器链表结构和定时器数组
static struct timer_list {
    long jiffies;                   // 定时滴答数
    void (*fn)();                    // 定时器处理程序
    struct timer_list * next;        // 下一个定时器
} timer_list[TIME_REQUESTS], * next_timer = NULL;

在中断处理函数中进行处理:

// 如果有用户的定时器存在,则将链表第 1 个定时器的值减 1。如果已等于 0,则调用相应的处理 
 // 程序,并将该处理程序指针置为空。然后去掉该项定时器
    if (next_timer) {
        next_timer->jiffies--;
        while (next_timer && next_timer->jiffies <= 0) {
            void (*fn)(void);

            fn = next_timer->fn;
            next_timer->fn = NULL;
            next_timer = next_timer->next;
            (fn)();
        }
    }

如果next_timer定时器链表存在那么就将链表中的第一个定时器的定时器滴答jiffies减1,如果jiffies到达0之后,则会创建一个函数指针fn指向该定时器的定时器处理程序fn,并且调用该函数最后将next_timer指向下一个定时器

这里可以看到只有前一个定时器jiffies到0之后才会执行下一个定时器,所以为了避免越后面的定时器越晚执行,使用的是差分时间链表

也就是说:

  • 第一个节点的 jiffies:距离现在还有多久
  • 第二个节点的 jiffies:距离第一个定时器到期后,还要再等多久
  • 第三个节点的 jiffies:距离第二个定时器到期后,还要再等多久 所以只递减第一个节点,效果等价于所有定时器的剩余时间都减少了一个滴答。

为什么只需要把第一个节点减 1

因为后面节点的真正剩余时间都包含第一个节点的值

设链表为:

d1 → d2 → d3

那么:

第一个定时器剩余时间 = d1
第二个定时器剩余时间 = d1 + d2
第三个定时器剩余时间 = d1 + d2 + d3

现在只执行:

d1–;

就会得到:

第一个剩余时间 = d1 – 1
第二个剩余时间 = d1 – 1 + d2
第三个剩余时间 = d1 – 1 + d2 + d3

可以看到,所有定时器的实际剩余时间都同时减少了 1。

因此不需要遍历整个链表,更不需要对每个节点执行 jiffies--

所以这里使用while而不是if,就是为了一次性把所有到达时间的定时器执行完成

进程时间片

do_timer的最后,有一段关于调度的代码(这里是针对单核处理器):

if ((--current->counter)>0) return;      // 当前线程还有剩余时间片,直接返回
current->counter=0;
if (!cpl) return;                         // 对于超级用户程序,不依赖 counter 值进行调度
schedule(); 

这里的counter是进程的时间片,当时间片还有剩余时,就不进行调度直接返回。因为 current->counter表示当前进程剩余的 CPU 时间片。只要时间片还没有用完,就没有必要启动调度器;时间片用完后,才需要让调度器重新决定接下来让哪个进程使用 CPU

这样避免每 10ms都进行一次完整调度,减少调度和进程切换的开销。

if (!cpl) return;这里的真正含义是

if (中断发生前正在内核态运行)
 暂时不进行抢占调度;

这与用户是否是 root 无关。普通用户进程执行系统调用时,也会进入 CPL 0 的内核态。

对于0.11内核的进程调度,找出时间片最大的进程对象进行调用,直至时间片为0,这个是优先级时间片轮转调度算法

上一篇
下一篇