[后台开发工程师总结系列] 2.操作系统之进程

进程

进程的概念和特征

进程结构一般由三部分组成:代码段、数据段和堆栈段。代码段用于存放程序代码数据,数个进程可以共享一个代码段。而数据段存放程序的全局变量、常量和静态变量。堆栈段中栈用于函数调用,它存放着函数参数、函数内部定义的局部变量。对斩断还包含了进程控制块(PCB)。PCB处于进程核心堆栈底部,不需要额外分配空间。PCB是进程存在的唯一标识。系统通过PCB的存在而感知进程的存在。

  • 进程是程序的一次执行
  • 进程是一个程序及数据在处理机上执行时所发生的活动
  • 进程是系统进行资源分配和调度的独立单位。进程的独立运行由进程控制块PCB控制和管理。程序段、相关数据、PCB三部分构成了进程映像。进程映像是静态的进程。
  • 进程具有动态性(创建、活动、暂停、终止过程、生命周期),并发性(多个进程在一段时间同时运行),独立性(进程是一个独立运行获得和调度资源的独立单位)、异步性(进程按照独自不可预知的速度前进)、结构性(每个进程都有一个PCB描述)

linux 系统的进程启动过程

整个linux是一个树形结构。树是系统自动构造的,即内核下的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1 号进程负责执行内核的部分初始化工作及系统配置,并创建若干个用于高数储存和虚拟管理的内核线程。

随后1号进程调用execve()运行可执行程序init , 并演变成用户态1号进程,它按照系统配置文件/etc/initab 的要求,完成系统的启动工作。并创建编号 1、2 和若干终端注册进程 getty。 当getty检测到来自终端的信号时, getty将通过 execve执行注册程序 login, 此时就可以通过用户名、密码登录。如果登录成功,login() 程序执行shell, shell进程接替 getty 进程的pid 取代getty进程。后续进程再通过shell产生

0号进程–》1号内核进程–》1号内核线程–》1号用户进程–》getty进程–》shell进程

进程状态及轮转

1550453785672

进程创建

创建状态:进程正在创建尚未就绪,创建经过几个步骤:申请空白PCB、向PCB写入控制和管理信息,然后为进程分配所需资源,最后转入就绪状态

引起进程创建的事件

系统创建(用户登录:分时系统中每个用户登录可以被看成一个新的进程。系统为该终端建立一个进程并插入就绪队列, 作业调度:批处理作业中,当系统按照一定算法调度作业时,将该作业调入内存为其分配资源,提供服务) 应用请求 (用户可以基于自己的需求创建新的进程)

进程创建的过程
  1. 为进程申请一个唯一的进程识别号与空白PCB
  2. 为进程分配资源,为新进程的程序、数据、用户栈分配内存空间
  3. 初始化PCB,主要包括标志信息、状态信息、处理机信息
  4. 如果就绪队列能够接受新进程,就将进程插入就绪队列中
Linux下的 进程创建

父进程和子进程:除了0号进程,linux系统中其他任何一个进程都是其他进程创建的。而相对的 ,fork 函数的调用方是父进程, 而创建的新进程是子进程。

fork 函数不需要参数,返回值是一个进程标识符

1 对于父进程,fork函数返回创建子进程ID

2 子进程 fork 函数返回0

3 创建出错的话 fork 函数返回 -1

fork函数创建一个新的进程,并从内核中为其分配一个可用的进程标识符PID,之后为其分配进程空间,并将父进程空间的内容中复制到子进程空间, 包括数据段和堆栈段,和父进程共享代码段。这时候系统中多了一个进程父进程和子进程都接受系统的调度,fork函数返回两次(分别在父进程和子进程中返回)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
pid_t pid;
pid = fork();
if(pid<0){
perror("fail to fork");
exit(-1);
}else if(pid ==0){
printf("Subprocess, PID: %u", getpid());
}else{
printf("Parentprocess, PID: %u", getpid());
}
return 0;
}

子进程和父进程共享数据段和堆栈段中的内容。例子略

事实上,子进程完全复制了父进程的地址空间,包括堆栈段和数据段。但是,子进程并未复制代码段,而是公用代码段。

进程终止

结束状态:进程从系统中消失,这可能是因为正常结束或其他原因中断退出。进程结束时,系统首先标志进程结束,然后进一步释放和回收资源。

进程终止的事件
  1. 正常结束
  2. 异常结束:出现某种错误导致无法运行,:越界、非法指令、运行超时等等
  3. 外界干预:进程应外界请求而终止
进程的终止过程
  1. 根据被终止的标识符,从PCB结合中检索出PCB并读取进程状态
  2. 若进程处于执行状态,立即终止并置标志为真
  3. 若进程还有子孙进程,则终止子孙进程防止其不可控
  4. 将终止进程的所有资源释放给系统或父进程
  5. 将被终止进程移出队列
  • 运行状态: 进程在处理机上运行
  • 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机以外的一切所需资源,只需得到处理机即可执行
  • 阻塞状态(等待,封锁状态):进程等待某一时间而暂停运行,即使处理机空闲也不嗯呢该运行

特殊的进程

僵尸进程和孤儿进程

在linux中 正常情况下子进程是通过父进程创建的, 子进程和父进程的运行是一个异步的过程。父进程无法预料子进程在何时结束,于是就产生了孤儿进程和僵尸进程。

孤儿进程,是指一个父进程退出后,它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1) 到进程所收养,并由init进程完成状态收集工作。

僵尸进程,是指一个进程使用fork创建子进程,如果子进程退出,而父进程没有用wait或waitpid调用子进程的状态信息,子进程的进程描述符仍在系统中,这种进程被称为僵尸进程。

简单理解为,孤儿是父进程已退出而子进程未退出;而僵尸进程是父进程未退出而子进程先退出。

为了避免僵尸进程,需要父进程通过wait函数来回收子进程

守护进程

linux系统中在系统引导时会开启很多服务,这些服务就叫做守护进程。为了增加灵活性,root可以选择开启的模式,这些模式叫做运行级别。守护进程是脱离于终端在后台运行的进程,守护进程脱离终端是为了避免进程在执行过程中在终端上显示并且不会被终端的信息打断。

守护进程是一个生存期较长的进程,通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。说话进程常常在系统引导装入时启动,linux系统有很多的守护进程,大多数服务都是通过守护进程实现的。如作业规划进程、打印进程。

在 linux 中每一个与用户交流的界面称为终端,每一个终端开始的进程都会依附于该终端,这个终端就被称为 进程的控制终端,当控制终端被关闭时,相应的进程都会被自动关闭。但是守护进程可以突破这种限制,它从被执行时开始运转,整个系统关闭时才推出。如果想让某个进程不因为用户或终端等变化受到影响,那么就需把一个进程变成一个守护进程。

创建一个守护进程的步骤如下所示:

1 创建子进程,父进程退出。

这是编写守护进程的第一步。由于守护进程脱离终端控制,因此在第一步完成后就会在终端里造成程序已经运行完毕的假象。之后所有的工作都在子进程完成,而与用户终端脱钩。

2 子进程中创建会话

这个步骤是创建守护进程最重要的一步,虽然他的实现十分简单,但是他的意义重大。这里使用的系统函数setid, 这里有两个概念:进程组和会话期

1) 进程组: 一个或多个进程的集合。进程组由进程组ID来唯一标识,除了进程号以外,进程组ID也是一个进程的必备属性,每个进程组都有一个组长进程,组长进程号等于进程组ID,而且进程组ID不会因为组成进程的退出而受到影响。

2) 会话周期: 会话期是一个或多个进程组的集合。通常一个会话开始于用户登录,终止于用户退出,再次期间运行的所有进程都输入这个会话期。

setid 函数用于创建一个新的会话,并担任该会话组的组长,调用 setid 有三个作用: 1 让进程摆脱原会话的控制 2 让进程摆脱原进程组的控制 3 让进程摆脱原控制终端的控制。

那么 创建守护进程为什么需要setid函数? 这是由于创建守护进程的第一步掉用了fork函数来创建子进程,再将父进程退出。由于调用fork函数时子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但是会话期、进程组、控制终端等还没有改变,所以还不是真正意义上的独立。

3) 改变当前目录为根目录

这一步骤也是必要的步骤,使用 fork 创建的子进程集成了父进程的工作目录,由于进程的运行过程中当前的目录是不能卸载的,这对于以后的使用造成诸多麻烦。通常的做法是将 “/” 变为守护进程的当前工作目录,这样就可以避免上述问题。

4) 重设文件权限掩码

文件权限掩码是指屏蔽掉文件权限中的对应位。

5) 关闭文件描述符

同文件权限码一样, 用fork函数创建的子进程会从父进程哪里继承一些已经打开的文件,这些文件节能永远不会被守护进程读写,但是他们一样消耗系统资源。所以需要关闭来自继承的文件描述符。

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
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXFILE 65535
int main(){
pid_t pc;
int i, fd, len;
char* buf = "this is a Dameon\n";
len = strlen(buf);
pc = fork();
if(pc<0){
printf("error fock\n");
exit(1);
}else if(pc>0){
exit(0);
}
setsid();
chdir("/");
umask(0);
for(int i=0; i<MAXFILE; i++){
close(i);
}
while(1){
if(fd = open('/temp/demeon.log',O_CREAT|O_WRONLY|O_APPEND,0600))<0){
perror("open");
exit(1);
}
write(fd, buf, len+1);
close(fd);
sleep(10);
}
return 0;
}

进程通信

进程间通信就是不同进程间传播或交换信息。 首先进程间可以通过传送、打开文件来实现,不同的进程通过一个或多个文件来传递信息。一般来说进程间通信不包括这种低级的通信方式。Linux操作系统几乎支持所有的UNIX系统进程通信方法:管道、消息队列、共享内存、信号量、套接字。

管道

父子进程通过管道通信,管道是一种两个进程间单向通信的机制。因为管道传递数据的单向性,管道又被称为半双工管道,管道这一特点决定了其使用的局限性。管道是最原始的一种通信方式。

  1. 数据只能由一个进程流向另一个进程(一个读管道和一个写管道);如果要进行双工通信,则需要建立两个管道。
  2. 管道只能用于父子通信或兄弟进程通信(有亲缘关系的进程)。

除了上述局限性,管道还有一个不足,比如管道没有名字(匿名管道);管道的缓冲区大小受限(linux 下一般是4KB);管道传输的是无格式的字节流。这就需要管道的输入方和输出方事先约定好数据格式。使用管道通信时,两端的进程向管道读写数据是通过创建管道时,系统设置的文件描述符进行的。本质上说管道也是一种文件,但是它又和一般的文件不同,可以克服文件通信的一些问题。

通过管道通信的两个进程,一个向管道写数据,一个从中读数据。写入的数据每次都添加到管道缓冲区的末尾,读数据都是从缓冲区的头部读出。

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
30
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INPUT 0
#define OUTPUT 1

int main(){
int fd(2);
pid_t pid;
char buf[256];
int returned_count;
pipe(fd);
pid = fock();
if(pid<0){
printf("Error in fork\n");
exit(1);
}else if(pid==0){
printf("in child process ...");
close(fd(INPUT));
write(fd(OUTPUT), "hello world", strlen("hello world"));
exit(0);
}else{
printf("int parent process ...");
close(fd(OUTPUT));
returned_count = read(fd(INPUT), buf, sizeof(buf));
printf("%d bytes of data received from child process: %s\n", returned_count, buf);
}
reutrn 0;
}

在子进程中写数据,在父进程中读数据,两个进程实现了通信:父子进程分别有自己的读写通道,为实现父子进程的通信,只需把无关的文件描述符关闭即可。

具名管道

还有一种管道叫做具名管道(FIFO)它不同之处是它提供一个路径名与之关联,以FIFO的形式存在于文件系统中。这样即使与FIFO创建不存在亲缘关系的进程,只要可以访问路径,就能够通过彼此的FIFO通信(能够访问路径和FIFO创建进程之间),因此通过FIFO不相关进程也能交换数据。

有名管道有以下特点

  1. 他可以使互不相关的两个进程实现通信
  2. 该管道可以通过路径名来指明,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样读写,使用很方面
  3. FIFO严格遵循先进先出的规则,对于管道与FIFO总是从开始处返回数据,而把数据添加到末尾。

消息队列

消息队列用运行在同一机器上的进程通信,与管道类似,是一个系统内核中保存消息的队列,在内核中以消息链表的形式出现。

消息队列与有名管道有不少相同之处,消息队列进行通信可以使不相关的进程,同时他们都是以发送和接收的方式来传递数据的。而且他们都有一个最大长度的限制。

与命名管道相比,消息队列的优势在于:1、消息队列可以独立于发送和接收进程存在,从而消除了同步命名管道时的困哪 2、同时发送消息避免了管道的同步和阻塞,不需要进程提供同步方法 3、接收端可以有选择 的接收数据。

事实上它是一种正在被淘汰的通信方式,完全可以用流管道和套接口的方式取代。

共享内存

共享内存允许两个不相关的程序访问同一个逻辑内存。共享内存是在两个正在运行的程序间共享和传递数据的一种非常有效的方式。不同进程间的共享内存通常安排在同一物理内存中。进程可以将同一段内存共享到自己的地址空间中,所有进程都可以访问共享 内存中的地址。

不过,共享内存未提供同步机制,需要进程自行进行同步操作。

共享内存的优缺点:

1:优点 使用共享内存通信非常方便,而且函数接口简单,数据共享还使进程间的数据不用传送,而是直接访问内存,加快了效率,并且没有亲缘关系的要求。

2:缺点 共享内存没有提供同步进制,这使得共享内存的通信往往要借助其他手段来完成。

信号量

共享 内存是进程间通信的最快的方式,但是共享 内存的同步问题自身无法解决(即进程该何时去共享内存取得数据,而何时不能取),但用信号量即可轻易解决这个问题 。

进程调度

调度层次

作业调度

高级调度,主要任务是按一定原则从外存中将处于后备状态的作业挑选1个或几个,分配内存、输入输出等资源,建立相应进程。使得他们拥有竞争处理机的权力(内存与辅存之间的调度)

中级调度

进程的挂起与就绪

进程调度

低级调度,某种方法和策略从就绪队列中选取一个进程,为其分配处理机。进程调度是最基本的调度,频率很高,一般几十毫秒一次

调度算法

先来先服务(FCFS)算法

FCFS是一种最简单的调度算法,从后备作业队列中选择最先进入该队列作业调度

FCFS是不可剥夺算法,长作业会使后到的短作业长期等待。

特点:算法简单,效率低,对长作业有利,有利于CPU繁忙性作业

短作业优先(SJF)算法

从后背队列中选择一个或若干个估计运行时间最短的作业调入内存运行

特点:对长作业不利,如果短作业源源不断,会使得长作业一直处于饥饿状态。

优先级调度算法

优先级调度算法每次从后背队列中选取优先级最高的一个或几个作业

特点:优先级调度可以剥夺式占有,也可以非剥夺式占有

高响应比优先

高响应比有限主要用于作业调度,该算法是对FCFS和SJF算法的一种平衡,计算每个作业的响应比。

响应比的计算为(等待时间+要求服务时间)/ 要求服务时间

时间片轮转调度算法

时间片轮转算法适用于分时系统,系统将所有就绪的进程按照到达时间排成一个序列,进程调度总是选择就绪队列中的第一个进程执行。但是仅能运行一个,如100ms。

特点:受系统响应时间影响、队列进程数目、进程长短影响较大

多级反馈队列调度算法

多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合和发展

1) 设置多个就绪队列,为各个队列赋予优先级,1、2、3等等

2) 赋予各个队列中时间片大小不同,优先级高时间片越小

3) 一个进程进入内存后首先放入1级队列末尾,FCFS原则等待,如果其能够完成,则撤离系统,否则放入2级队列的末尾,依次向下执行

4) 仅当1级队列为空时,调度程序调度2级队列中的进程,依次类推。

临界区

虽然多个进程可以共享系统中的资源,但许多资源一次只能被一个进程使用,把一次仅允许一个进程使用的资源称为临界资源。

// entry

// critical section

// exit section

同步与互斥

同步:进程之间具有直接制约关系,进程之间需要按照一定的次序进行

互斥:进程之间的间接制约关系,不能同时访问临界区

信号量

信号量是一个整形变量,可以被定义为两个标准的原语wait(S) signal(S) 即P、V操作

  • P操作 如果信号量大于0,执行 -1操作,如果等于0,执行等待信号量大于0
  • V操作 对信号量完成加1操作,唤醒睡眠的进程
1
2
3
4
5
6
7
8
9
10
11
12
typedef int semaphore
semaphore mutex = 1
void P1(){
P(&mutex);
//临界区
V(&mutex);
}
void P2(){
P(&mutex);
//临界区
V(&mutex);
}

使用信号量实现生产者-消费者问题

问题描述:使用一个缓冲区来保存物品,只有缓冲区没满,生产者才可以放入物品;只有缓冲区不空,消费者可以拿走物品

由于缓冲区输入临界资源,需要一个互斥量mutex来完成缓冲区的互斥访问

为了同步生产者和消费者的行为,需要记录缓冲区物品数量,数量可以用信号量表示,empty记录空缓冲区,full记录满缓冲区

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
# define N 100
typedef int semahpore
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer(){
while(True){
int item = produceItem();
P(&empty);
P(&mutex);
Item.push(item);
V(&mutex);
V(&full);
}
}

void consumer(){
while(True){
P(&full);
P(&mutex);
int item = Item.top();
Item.pop();
consume(item);
V(mutex);
V(&empty())
}
}

管程

使用信号量机制生产消费问题客户端代码需要很多控制,管程作用是把控制的代码独立出来。

管程有一个重要作用:一个时刻只能有一个进程使用。进程不能一直占用管程,不然其他程序都无法使用

管程的生产者消费者实现

读者-写者问题

问题描述: 控制多个进程对数据进行读、写操作,但是不允许读-写和写-写操作同时进行

用一个count表示读进程数量,分别用read_mutexwrite_mutex 作为读锁和写锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef int semaphore
semaphore count = 0;
semaphore read_mutex = 1;
semaphore write_mutex = 1;

void read(){
P(&read_mutex);
count++;
if(count==1) P(&write_mutex);
V(&read_mutex);
read();
p(&read_mutex);
count--;
if(count==0) V(&write_mutex);
V(&read_mutex);
}

void write(){
P(&write_mutex);
write();
V(&write_mutex);
}

哲学家进餐问题

问题描述:五个哲学家围着一张圆桌,每个哲学家面前放着食物,哲学家有两种活动:吃饭与思考,吃饭时,他拿起左边及右边的筷子,并且一次只能拿一根

如果所有哲学家都拿左边的筷子,就会出现死锁,这样只需加一步,当哲学家拿起筷子时检查是否能同时拿起两根筷子,不然就等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef int semaphore
semaphore chop[5] = {1,1,1,1,1};
semaphore mutex = 1;

void process(){
while(true){
P(&mutex);
P(chop[i]);
P(chop[(i+1)%5]);
V(&mutex);
eat();
V(chop[i]);
V(chop[(i+1)%5]);
}
}

死锁

死锁的定义:多个进程因为竞争资源而造成的一种僵局(互相等待),若无外力作用,所有的进程都无法向前推进。

死锁四个必要条件:

  • 互斥条件:进程要求对所分配的资源进行排他性控制,在一段时间内资源仅为一个进程所有。
  • 不剥夺条件:进程所获得资源未使用完毕之前,不能被其他进程强行夺走,只能等获得资源的进程自己主动释放
  • 请求和保持条件:进程已经至少保持了一个资源,但是又提出了新的资源请求,而该资源已被其他进程占有。此时进程被阻塞,但是对自己资源不释放。
  • 循环等待条件:存在某一进程的循环等待链,链中每个进程已获得资源下个进程的请求。

死锁的处理策略

死锁的处理便是破坏四个必要条件,使得死锁无法发生

鸵鸟策略

把头埋在沙子里,假装问题没有发生

由于解决死锁问题的代价往往很高,鸵鸟策略在很多情况下可以取得更高的性能。

大多数操作系统,Unix、Linux、windows处理死锁都是采用鸵鸟策略

死锁预防
  1. 破坏互斥条件

    对于可共享的资源竞争,不会发生死锁

  2. 破坏不剥夺状态

    当一个进程无法获取其需要的资源时,将之前已获得的资源释放,待需要是再重新申请

  3. 破坏请求 和 保持条件

    预先分配的静态方法,在进程运行前一次申请完它需要的所有资源。在资源不满足前不运行,一旦运行这些资源都归期所有。

  4. 破坏循环等待

    资源顺序分配法,例如为资源编号,每个进程申请分配某个资源以后,再之后只能申请该编号以后的资源

死锁避免

系统的安全状态:所谓安全状态,是系统能按照某种进程推进顺序(P1,P2,,)为每个进程分配资源,直至满足每个进程对资源的最大需求,使每个系统进程都能顺序完成,则(P1、P2,,)称为安全序列。如果无法找到安全序列,则系统处于不安全状态。

允许进程池动态的申请资源,但是每次分配资源前系统都会计算资源分配的安全性,如果分配资源不会导致系统进入不安全状态,将资源分配给进程;否则,进程等待

银行家算法

img

银行家算法是最著名的死锁避免算法。它的思想是,把操作系统看成银行家,操作系统管理的资源当成银行家管理的资金,向操作系统请求资源相当于向银行请求贷款。

进程申请资源时,系统评估该进程的最大需求资源,检查资源分配后系统是否还处于安全状态,由此来决定是否分配该资源

死锁检测和接触

死锁检测

死锁定理:

可以通过将资源分配图简化的方法来检测系统状态 S 是否为死锁状态。简化方法如下:(1)、在资源分配图中,找到既不阻塞又不是孤点的进程 Pi (即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量)。消去它所有的请求边和分配边,使之成为孤立的结点。在这里要注意一个问题,判断某种资源是否有空闲,应该用它的资源数量减去它在资源分配图中的出度。(2)、进程 Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可以变为非阻塞进程。根据(1)中的方法进行一系列简化后,若能消去图中所有的边,则称该图是可完全简化的。

S为死锁的条件是:当且仅当 S 状态的资源分配图是不可完全简化的,该条件为死锁定理。

死锁解除
  • 资源剥夺法

    挂起死锁进程,抢占其资源分配给其他进程

  • 撤销进程法

    强制撤销一些死锁进程

  • 进程回退法

    借助历史信息使一个或多个进程回退到系统不再死锁的地步

问题简析

1 同步和异步的区别

同步异步通常用来形容一次方法调用。

  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作。

2 进程和线程的区别,谁调度的进程

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。
  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

3 死锁的条件,如何检测死锁

见上文死锁条件

死锁定理:

可以通过将资源分配图简化的方法来检测系统状态 S 是否为死锁状态。简化方法如下:(1)、在资源分配图中,找到既不阻塞又不是孤点的进程 Pi (即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量)。消去它所有的请求边和分配边,使之成为孤立的结点。在这里要注意一个问题,判断某种资源是否有空闲,应该用它的资源数量减去它在资源分配图中的出度。(2)、进程 Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可以变为非阻塞进程。根据(1)中的方法进行一系列简化后,若能消去图中所有的边,则称该图是可完全简化的。

S为死锁的条件是:当且仅当 S 状态的资源分配图是不可完全简化的,该条件为死锁定理。
4 死锁的必要条件,银行家算法

见上文死锁条件

见上文银行家算法

5 调度算法有哪些

见上文调度算法

6 通俗的语言,面对一个非程序员,解释进程与线程的区别

个人理解:把进程和线程比作一家公司和公司的员工,类比如下:

  1. 进程是分配资源的最小单位(资金、材料、工具属于公司),而线程是最小的调度单位(公司领导指派某个人去工作)
  2. 进程有独立的地址空间,独立的代码段、堆栈段和数据段,申请昂贵(公司有独立的地址、办公室、公章、注册单位)而且进程间切换代价大,一个进程内的线程切换十分方便(同一个公司的员工互相调动很方便)
  3. 线程间通信方便,由于线程共享堆栈、数据段(公司内部沟通方便)而进程间沟通需要通过IPC方式,还需要处理同步、互斥,这也是多线程编程的难点。
  4. 而多进程更加的健壮,进程间并不互相依赖。(公司A和公司B都可以独立完成一个项目)

7 死锁是什么,为什么会产生死锁,怎么解决死锁问题,预防死锁、避免死锁

死锁的定义:多个进程因为竞争资源而造成的一种僵局(互相等待),若无外力作用,所有的进程都无法向前推进。

死锁有四个必要条件:互斥、不剥夺、请求和保持、循环等待

破坏这些条件,包括:资源剥夺、撤销进程、进程回退

可以采用银行家算法预防和避免死锁

8 进程的同步进制有哪些? 进程的通信机制有哪些?

临界区、互斥量、信号量、事件

临界区: 通过多线程的串行化来保证某一时刻只有一个线程能访问资源或代码,适合控制数据访问,只能控制同一进程中的线程

互斥量: 为协调共享资源设计,互斥对象只有一个,只有拥有互斥对象的线程可以访问资源

信号量:允许多个线程访问同一资源,但是限制线程数目。适用于跨进程同步,功能强大。

事件:通知线程有事情发生, 启动后续任务。

进程通信:

管道:父子进程通过管道通信,管道是一种两个进程间单向通信的机制。因为管道传递数据的单向性,管道又被称为半双工管道,管道这一特点决定了其使用的局限性。管道是最原始的一种通信方式。

具名管道(FIFO):还有一种管道叫做具名管道(FIFO)它不同之处是它提供一个路径名与之关联,以FIFO的形式存在于文件系统中。这样即使与FIFO创建不存在亲缘关系的进程,只要可以访问路径,就能够通过彼此的FIFO通信(能够访问路径和FIFO创建进程之间),因此通过FIFO不相关进程也能交换数据。

消息队列:消息队列用运行在同一机器上的进程通信,与管道类似,是一个系统内核中保存消息的队列,在内核中以消息链表的形式出现。消息队列与有名管道有不少相同之处,消息队列进行通信可以使不相关的进程,同时他们都是以发送和接收的方式来传递数据的。而且他们都有一个最大长度的限制

共享内存:共享内存允许两个不相关的程序访问同一个逻辑内存。共享内存是在两个正在运行的程序间共享和传递数据的一种非常有效的方式。不同进程间的共享内存通常安排在同一物理内存中。进程可以将同一段内存共享到自己的地址空间中,所有进程都可以访问共享 内存中的地址。

信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

9 进程的状态转换图及转换事件

见上文