Linux进程与线程的学习

1、进程的概念。

一个程序被加载到内存中运行,那么在内存内的那个数据被称为进程。进程是操作系统非常重要的概念。在一个操作系统中,可以同时运行多个进程,对于单处理器来说,并发执行指的是在同一时间间隔内同时执行多个进程。

每个进程都好像独占使用硬件,这是因为进程还有一个抽象概念——虚拟存储器,也是虚拟地址空间。操作系统会为每一个进程创建一个一定大小的虚拟地址空间,虚拟空间之间是互相独立的,这使得进程之间相互隔离了,每个虚拟地址空间包括内核空间和用户空间。

那么问题来了,为什么会存在虚拟地址空间呢?

早期的计算机,程序是直接加载到物理内存中,当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。更具体的说,是当CPU执行一条指令时,它会生成一个有效的物理地址,通过存储器总线,把它传递给主存,主存会取出对应的字节的字,并将它返回给CPU,CPU会将它存放到寄存器里。这种做法,会引起很多问题,比如地址不隔离,会导致恶意程序篡改;内存使用效率低等等。具体的可以看一个哥们写的博客介绍的挺好: Linux虚拟地址空间

虽然后来有出现了兑换和覆盖等技术,但还是不能真正解决内存利用率低的问题。基于此,后来就出现了虚拟存储的空间。虚拟存储就是同时利用内存和外存一起实现的一种方式。在进程执行时,会将暂时不需要的程序或数据放在内存外,在需要时通过缺页异常获取数据。具体在我写的操作系统内存管理有提到这个,可以参考: 操作系统内存管理

虚拟地址空间是连续的,大小是固定的,对于32位操作系统,会分配2^32,即4GB的空间;对于64位操作系统,则会分配2^64。而一个虚拟地址空间由内核空间和用户空间组成,比例为1:3。最上面的内核空间是预留给操作系统中的代码和数据的,每一个进程都是相同的。底部的地址空间用来存放用户进程定义的代码和数据。

局部变量、函数参数、返回地址等

动态分配的内存

BSS段

未初始化或初值为0的全局变量和静态局部变量

数据段

已初始化且初值非0的全局变量和静态局部变量

代码段

可执行代码、字符串字面值、只读变量

上面这个,以前笔试或者面试考过。

1、栈

编译器用它来实现函数调用,它是在程序执行期间动态地扩展和收缩。当我们调用函数时,栈会增长,每当函数返回时,栈就会收缩。它是向下增长的,且是连续的。

2、堆

在运行时是由malloc创建的,也就是用户。可以在运行中动态地扩展或收缩。它是向上增长的,且不是连续的。

3、程序代码和数据

代码是从以固定地址开始的,然后是数据段,存储全局变量。代码和数据区是直接初始化的,大小固定。

2、进程的状态

一个进程包括3种状态:就绪、运行、阻塞。

1、就绪

万事俱备只欠东风啦。进程已经准备好了运行的所有资源,就等着系统分配处理器来处理了。

2、运行

进程占有处理器正在运行。

3、阻塞

当正在执行的进程运行期间,由于所期待的事情未发生,如(请求资源失败,某种操作的完成,新数据尚未到达等),则由系统自动执行阻塞原语,使进程自己由运行变为阻塞状态,由此可见,进程阻塞是进程自身的一种主动行为,因此只有运行的进程才能进入阻塞状态,处于阻塞状态的进程是不占用CPU资源的。

那他们之间的状态转换时什么时间触发的呢,看下面的状态转换说明:

1)就绪——执行:对就绪状态的进程,当系统为其分配了一个处理器来处理的时候,该进程便由就绪状态变为执行状态;
2)执行——阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如进程提出输入/输出请求而变成等待外部设备传输信息的状态,进程申请资源(主存空间或外部设备)得不到满足时变成等待资源状态,进程运行中出现了故障(程序出错或主存储器读写错等)变成等待干预状态等等;
3)阻塞——就绪:处于阻塞状态的进程,在其等待的事件已经发生,如输入/输出完成,资源得到满足或错误处理完毕时,处于等待状态的进程并不马上转入执行状态,而是先转入就绪状态,然后再由系统进程调度程序在适当的时候将该进程转为执行状态;
4)执行——就绪:正在执行的进程,因时间片用完而被暂停执行,或在采用抢先式优先级调度 算法 的系统中,当有更高优先级的进程要运行而被迫让出处理机时,该进程便由执行状态转变为就绪状态。

当进程从运行状态变为阻塞状态,然后处理器去处理别的进程,操作系统实现这种交错执行的机制成为上下文切换。

操作系统保存进程运行所需要的所有状态信息。这种状态,也就是上下文,包括所有信息。在单CPU下,在任何一个时刻,系统只有一个进程在运行(当然,在这里不包括当所有进程恰好都处于死锁的情况,那么就是0个)。当操作系统决定从当前进程转移控制权到其他进程时,就会进行上下文切换,即保存当前进程的上下文,回复新进程的上下文,然后转移控制权给新进程。新进程会从上次停止的地方开始执行。

切换过程如下:

      1:保存处理机上下文,包括程序计数器和寄存器
      2:更新PCB信息
      3:把进程的PCB移入相应的队列,
      4:选择另一个进程执行,并更新其PCB
      5:更新内存管理的数据结构
      6:恢复处理机上下文

这的最后还要补充一个概念,就是僵尸进程。  僵尸进程一般是该进程应该已经执行完毕,或者应该停止了,但该进程的父进程却无法完全将该进程杀掉,而使得这个进程一直存在于内存中。

查看僵尸进程的方法是如果ps查看进程后面跟着<defunct>就代表僵尸进程(这段是摘抄,没测试过)。

3、进程的创建

在Linux中,所有进程也是一个树形结 构。树根是系统自动构造的,即在内核态下执行的0号进程,它是所有进程的祖先。

由0号进程创建1号进程(内核态)1号进程是开机时,内核调用的初始化进程-init。该进程负责准备操作环境,启动各种服务,执行rc.local等初始化文件。

除了0号和1号进程,其他进程都是通过fork创建的。即过程调用过程是先通过父进程以fork的方式复制一个与父进程相同的暂存进程,然后暂存金城开始以exec的方式加载实际要执行的程序。

ps ajxf    查看进程树

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
   
 1 28716 28716 28716 ?           -1 Ss       0  11:04 /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/supervisord -c supervisor.conf
28716 30684 30684 28716 ?           -1 S        0   4:43  \_ /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/celery -A dailyblog worker --loglevel=INFO
30684 30690 30684 28716 ?           -1 S        0   0:00  |   \_ /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/celery -A dailyblog worker --loglevel=INFO
28716 24372 24372 28716 ?           -1 S        0   0:07  \_ /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/gunicorn dailyblog.wsgi:application -c /srv/dailyblog/www/
24372 24377 24372 28716 ?           -1 Sl       0   0:14      \_ /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/gunicorn dailyblog.wsgi:application -c /srv/dailyblog/
24372 24380 24372 28716 ?           -1 Sl       0   0:18      \_ /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/gunicorn dailyblog.wsgi:application -c /srv/dailyblog/
24372 24383 24372 28716 ?           -1 Sl       0   0:19      \_ /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/gunicorn dailyblog.wsgi:application -c /srv/dailyblog/
24372 24384 24372 28716 ?           -1 Sl       0   0:25      \_ /srv/dailyblog/env/bin/python /srv/dailyblog/env/bin/gunicorn dailyblog.wsgi:application -c /srv/dailyblog/

第一个gunicorn是主进程,下面的都是fork出来的子进程。

4、进程的管理

进程创建:在Unix系统中会使用fork/exec来创建进程,通过fork,会复制当前进程出一个子进程,子进程完全复制父进程的地址空间。int pid = fork(),父子进程都会走到一个地方,但返回的Pid不同,父进程会返回子进程pid,子进程会返回0.

进程的退出可以通过调用exit完成,进程退出会释放所有资源,包括内存,内核数据结构,关闭所有文件等等,此外也会检查父进程是否存活,如果存活,会把结果返回给父进程。

进程间通信IPC

进程间通信主要是进程之间进行信息交互以及同步的一种机制。目前存在的进程间通信方式FIFO,信号量、消息表、PIPE管道、共享内存等等。

线程

多进程已经实现了多道程序,一定程度支持并发,但是由于地址空间隔离,导致进程间数据共享是个问题。此外,进程间的切换成本很高。基于此,出现了线程。线程出现的目的就是能够减小切换成本,可以轻松实现数据的共享。

一个进程内的多个线程共享进程的资源,只是各自有着独立的栈和寄存器,用于执行指令流。线程是CPU调度的基本执行单位。

线程主要分为用户线程和内核线程。

内核线程是由内核管理线程,不用用户程序支持,也是目前Linux,windows采用的方法;用户线程

是由用户自行管理,通过函数调用实现,CPU是无感知的。我感觉这有点类似协程的做法。

--------EOF---------
微信分享/微信扫码阅读