进程概念

进程

程序本身不是进程,程序只是一种存储指令的文件,是静态的,相反,进程本身是动态的。
进程不是执行的程序,进程不只是程序代码,进程还包括程序计数器,堆栈,数据段等
进程本身可以作为一个环境,用于执行其他代码(比如Java的JVM)

进程状态

进程在执行时会改变状态,取决于进程的当前活动,状态如下(进程的五态模型

  • new :创建进程
  • running: 执行进程/指令
  • waiting:进程等待发生事件(比如IO信号),也叫阻塞状态
  • ready:进程等待分配处理器
  • terminated:完成执行
    image.png
    image.png

    进程控制块

    操作系统内的每个进程表示,采用进程控制块(Process Control Block, PCB),也称为任务控制块,它包含了许多与特定进程的相关信息
  • 进程状态:上面的进程五态模型
  • 程序计数器:进程将要执行的下个指令的地址
  • CPU寄存器:寄存器的数量与类型取决于计算机体系结构,存储了许多进程的信息包括累加器,寄存器,指针等。这些状态使得进程在中断后能够正确的执行
  • CPU 调度信息:包括进程优先级,调度队列指针等
  • 内存管理信息:
  • 说明信息(accounting information):使用时限,CPU使用时间,进程ID等
    image.png

    线程

    线程是进程中的执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源( 程序计数器,一组寄存器和栈),但它可与同属一个进程的其他线程 共享进程所拥有的全部资源。

    线程的优点

  • 易于调度。
  • 提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一个程序的不同部分。
  • 开销少。创建线程比创建进程要快,所需开销少,占用的资源也少;
  • 充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分的运行。

    线程和进程的区别

  • 调度:线程作为处理器调度和分配的基本单位,而进程是作为拥有资源的基本单位

  • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行

  • 拥有资源:进程是拥有资源的一个独立单位,有自己独立的地址空间;线程不拥有系统资源,但可以访问隶属于进程的资源,共享进程的地址空间.

  • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

    线程和进程的联系

  • 二者均可并发执行.

  • 线程是指进程内的一个执行单元,也是进程内的可调度实体。一个程序至少有一个进程,一个进程至少有一个线程,一个线程只属于一个进程.

  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。

  • 处理机分给线程,即真正在处理机上运行的是线程。

  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.进程和线程的主要差别在于它们是不同的操作系统资源管理方式。 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。而线程只是一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间, 一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

    进程调度

    为了无论何时都有进程在CPU上运行,最大化CPU利用率,进程调度器选择一个可用进程到CPU上执行。如有多个进程,剩下的需要到CPU空闲后重新等待调用。

    调度队列

    一般的没有优先级考虑的调度队列模型如下:
    image.png
    如上图,ready queue是就绪队列,用单向链表实现,有两个指针,头指针指向第一个 PCB块,尾指针指向最后一个PCB块。PCB块之间有单向链表链接。
    如果多个进程向一个设备发送了I/O请求,那么就需要在设备间进行排队,这个队列称为设备队列。如上图, disk就拥有三个进程等待读写,每个设备拥有自己的设备队列。
    image.png
    上图拥有两种队列:就绪队列和设备队列。新进程被分配到就绪队列,在被CPU执行时会发生下面三种情况

  • 进程发出 I/O 请求,放到 I/O 队列中
  • 创建新的子进程,等待子进程终止
  • 由于被CPU强制中断,回到就绪队列中
    前两种情况,进程会从阻塞状态切换到就绪状态,并放回就绪队列。

    调度器

    进程会在各种调度队列之间迁移,因此操作系统需要有一种选择方式从队列中选择进程。进程选择由调度器来执行
    对于批任务处理系统来说,多数被提交的进程是无法立即执行的,因此保存到大容量存储设备中,由长期调度器执行
    短期调度器负责选择就绪队列中的进程交给CPU执行。

短期调度器与长期调度器之间的区别在于执行的频率。短期调度器必须时刻为CPU选择新的进程,进程可能就执行几毫秒就切换。因此对于性能非常重要。而长期调度器执行并不频繁,可能是几分钟的间隔。

长期调度程序应该选择 I/O密集型和 CPU密集型的程序进行合理的进程组合

在分时系统中可能会引入中期调度器,将进程从内存中移出以降低多任务的繁忙程度。这种方案称为交换(swap)
image.png

上下文切换

切换 CPU 到另一个进程需要保存当前进程和恢复另一个进程的状态,这个任务称为上下文切换
上下文切换的时间与硬件支持密切相关,操作系统越复杂,上下文切换就需要做的更多。

进程运行

1
ps -el

上列命令可以列出系统中的所有当前活动进程的完整信息,通过递归跟踪父进程可以轻松构造进程树

进程创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
pid_t pid;

/* fork a child process */
pid = fork();

if (pid < 0) { /* error occurred */
fprintf(stderr, "Fork Failed\n");
return 1;
}
else if (pid == 0) { /* child process */
printf("I am the child %d\n",pid);
execlp("/bin/ls","ls",NULL);
}
else { /* parent process */
/* parent will wait for the child to complete */
printf("I am the parent %d\n",pid);
wait(NULL);

printf("Child Complete\n");
}

return 0;
}

在这里使用书上的图3-9的例子进行说明,下面是运行结果。(实验环境Kali-Linux2023.2,gcc版本13.1.0)
image.png
image.png
可以看到父进程的pid是大于0的,子进程的pid是等于0的,这也是子进程和父进程之间唯一的不同。(实际上,子进程的pid就是父进程的pid)
父进程通过wait方法等待子进程的完成,子进程通过执行 $ls$ 命令,换句话说,子进程用 $ls$ 系统命令代替了其本身,因此最多只会执行一次 $ls$ 命令,之后就会直接退出。
父进程等待子进程执行完命令之后唤起,然后退出。

注:exec是一个函数族,有六个函数,执行 exec 时新程序会替换进程的堆栈代码段等。如果还想执行下面程序可以使用 system 系统调用
Linux系统编程——进程替换:exec 函数族 - 知乎 (zhihu.com)

进程终止

孤儿进程

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(pid=1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

危害

如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程

参考

Operating System Concepts - 10th edition (os-book.com)
操作系统:进程与线程之间的区别及联系 - 知乎 (zhihu.com)
孤儿进程与僵尸进程[总结] - Rabbit_Dale - 博客园 (cnblogs.com)