2、进程、线程、中断的核心:栈

1 进程、线程、中断的核心:栈

中断中断,中断谁?

中断当前正在运行的进程、线程。

进程、线程是什么?内核如何切换进程、线程、中断?

要理解这些概念,必须理解栈的作用。

1.1 ARM处理器程序运行的过程

ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:

① 对内存只有读、写指令

对于数据的运算是在CPU内部实现

使用RISC指令的CPU复杂度小一点,易于设计

比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:

 

 

 

细看这几个步骤,有些疑问:

a,那么a的值读出来后保存在CPU里面哪里?

b,那么b的值读出来后保存在CPU里面哪里?

③ a+b的结果又保存在哪里?

 

我们需要深入ARM处理器的内部。简单概括如下,我们先忽略各种CPU模式(系统模式、用户模式等等)。

注意:如果想入理解ARM处理器架构,应该从裸机开始学习。我们即将写好近30个裸机程序的文档,估计还3月底发布。

注意:为了加快学习速度,建议先不看裸机。

 

CPU运行时,先去取得指令,再执行指令:

① 把内存a的值读入CPU寄存器R0

② 把内存b的值读入CPU寄存器R1

③ 把R0R1累加,存入R0

④ 把R0的值写入内存a

1.2程序被中断时,怎么保存现场

从上图可知,CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。

保存在哪里?内存,这块内存就称之为栈。

程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。

这个场景并不局限于中断,下图可以概括程序A、B的切换过程,其他情况是类似的:

 

 

 

 

 

a. 函数调用:

在函数A里调用函数B,实际就是中断函数A的执行。

那么需要把函数A调用B之前瞬间的CPU寄存器的值,保存到栈里;

再去执行函数B;

函数B返回之后,就从栈中恢复函数A对应的CPU寄存器值,继续执行。

b. 中断处理

进程A正在执行,这时候发生了中断。

CPU强制跳到中断异常向量地址去执行,

这时就需要保存进程A被中断瞬间的CPU寄存器值,

可以保存在进程A的内核态栈,也可以保存在进程A的内核结构体中。

中断处理完毕,要继续运行进程A之前,恢复这些值。

c. 进程切换

在所谓的多任务操作系统中,我们以为多个程序是同时运行的。

如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程A的时间用完了,就切换到进程B。

怎么切换?

切换过程是发生在内核态里的,跟中断的处理类似。

进程A的被切换瞬间的CPU寄存器值保存在某个地方;

恢复进程B之前保存的CPU寄存器值,这样就可以运行进程B了。

 

所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。

进程的调度也是使用栈来保存、恢复现场:

 

 

1.3进程、线程的概念

假设我们写一个音乐播放器,在播放音乐的同时会根据按键选择下一首歌。把事情简化为2件事:发送音频数据、读取按键。那可以这样写程序:

int main(int argc, char **argv)

{

  int key;

  while (1)

  {

    key = read_key();

    if (key != -1)

    {

      switch (key)

      {

        case NEXT:

            select_next_music(); // GUI选中下一首歌

            break;

       }

      }

     else

     {

      send_music();

     }

    }

    return 0;

}

这个程序只有一条主线,读按键、播放音乐都是顺序执行。

无论按键是否被按下,read_key函数必须马上返回,否则会使得后续的send_music受到阻滞导致音乐播放不流畅。

读取按键、播放音乐能否分为两个程序进行?可以,但是开销太大:读按键的程序,要把按键通知播放音乐的程序,进程间通信的效率没那么高。

这时可以用多线程之编程,读取按键是一个线程,播放音乐是另一个线程,它们之间可以通过全局变量传递数据,示意代码如下:

int g_key;

void key_thread_fn()

{

  while (1)

  {

    g_key = read_key();

    if (g_key != -1)

    {

      switch (g_key)

      {

        case NEXT:

          select_next_music(); // GUI选中下一首歌

          break;

      }

    }

  }

}

void music_fn()

{

  while (1)

  {

    if (g_key == STOP)

      stop_music();

    else

    {

           send_music();

    }

  }

}

 

int main(int argc, char **argv)

{

  int key;

  create_thread(key_thread_fn);

  create_thread(music_fn);

  while (1)

  {

    sleep(10);

  }

  return 0;

}

这样,按键的读取及GUI显示、音乐的播放,可以分开来,不必混杂在一起。

按键线程可以使用阻塞方式读取按键,无按键时是休眠的,这可以节省CPU资源。

音乐线程专注于音乐的播放和控制,不用理会按键的具体读取工作。

并且这2个线程通过全局变量g_key传递数据,高效而简单。

 

Linux中:资源分配的单位是进程,调度的单位是线程。

也就是说,在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。

而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。如下图示:

 

 

 

 

调度的单位是线程,‌这是因为线程是操作系统中进行运算调度和执行的基本单位。‌线程被包含在进程之中,‌是进程中的实际运作单位,‌一条线程指的是进程中一个单一顺序的控制流。‌一个进程中可以并发多个线程,‌每条线程并行执行不同的任务。‌线程可以是内核线程,‌由操作系统内核调度,‌也可以是用户线程,‌由用户进程自行调度,‌或者是内核与用户进程混合调度。‌

 

线程与进程的区别和联系在于:‌

 

  • 进程是资源分配的基本单位,‌每个进程都有独立的内存空间(‌虚拟地址空间)‌,‌而线程则共享所属进程的虚拟地址空间。‌
  • 线程不能独立于进程而存在,‌一个进程里可以有一个线程,‌也可以有多个线程。‌
  • 线程的引入显著提高了系统的并发程度,‌使得在同一进程中多个线程的切换不会引起进程切换,‌从而提高了系统资源的使用效率和吞吐量。‌
  • 线程基本上不拥有资源,‌但它可以访问其所属进程的全部资源,‌如代码段、‌数据段及系统资源。‌
  • 创建或撤销线程的开销远小于创建或撤销进程的开销,‌因为线程切换不需要涉及存储管理的操作,‌而进程切换则需要保存和恢复执行环境,‌开销较大。‌

 

因此,‌将调度的单位定为线程,‌可以更有效地利用系统资源,‌提高系统的并发性和效率

 

posted on 2024-07-21 16:22  拉风摊主  阅读(30)  评论(0编辑  收藏  举报

导航