process
进程:正在被执行的程序。
进程=程序+数据+PCB
进程是现代分时操作系统的工作单元,包含内核进程和用户进程。
多个进程可以同时执行,cpu在不同的进程之间切换,可以使计算机更高产。
3.1 process concept
关于cpu正在进行的活动应该叫什么,批处理系统称之为job,分时系统称之为用户程序或任务。一般来说,process和job这两个词可以互换.
进程相比于程序来说,表示的更多,通常包括程序计数器,寄存器内容,进程堆栈,数据区,以及动态分配的堆内存。
多个进程可能是由相同的程序运行而来,但是这些进程往往并不相同。
一个进程可能成为其他进程运行的环境,jvm就是最好的例子。
进程在执行的过程中,会有多个状态。包括:new,running,waiting,ready,terminated。
PCB (process control block)
在操作系统中,每个进程都被一个pcb表示,包含与一个进程相关的很多信息,如:
process state
program counter
cpu registers
cpu sheduling information,如priority
memeory management information
accounting information,cpu使用相关的统计信息
I/O status information
线程
现代操作系统允许一个进程包含多个线程。在多核cpu上,多个线程可以在不同的cpu上同时执行。PCB必须进行扩展,包含进程所有创建的线程相关的信息
3.2 process scheduling
multiprogram的目标是最大化cpu的利用率。
分时切换的目标是能尽快的在进程之间切换,能够使每个进程都感觉自己独占cpu。
每个被加载进内存的进程都会被放入某个队列中,队列有很多种,如准备好的队列,等待某种特定设备的队列。
在进程的整个生命周期中,会在不同的进程队列中间移动。负责移动的就是进程调度器。
长期调度器负责将进程从磁盘移动到内存,短期调度器负责合理的分配cpu给不同的进程。他们之间主要的不同是短期调度器可能最多每隔100ms就必须执行一次,所以执行需要非常快。而长期调度器可能几分钟才执行一次。长期调度器必须要合理的选择I/O型和计算型的进程,确保cpu的利用率。
当内存不足以将所有进程都加载进来的时候,就需要用到交换技术,将内存中的进程写回到磁盘中,在合适的时候又重新读回内存,然后继续执行。
长期调度器决定了多道程序的道数。
短期调度器负责从ready队列中选择合适的程序在CPU上运行,执行的频率要更大一些。
context switch
当发生进程切换时,需要将当前进程的状态保存下来,以便之后进行恢复。保存到pcb中的信息包括:cpu寄存器,进程状态,内存管理信息。
context switch的时间随操作系统的不同而不同,一般在几毫秒。硬件对此影响非常大,比如说是否提供了设置多个寄存器的指令。以及操作系统的复杂度,越复杂耗时越久。
3.3 operations on processes
3.3.1 process creation
进程在生命周期过程中会创建子进程,最终会形成一颗树。每个进程都有一个唯一的编号。新创建的进程可能可以自已从操作系统申请资源,也有可能只能从父进程申请资源。
父子进程之前有三种资源共享模式:
1)共享所有资源
2)子进程从父进程处共享部分资源
3)不共享任何资源
创建子进程后,父进程可能等待子进程的运行,也有可能和子进程同时运行。
在unix系统中,调用fork()函数创建新的进程。fork函数会完全的将此进程的信息复制一份,唯一的区别就是pid的不同。然后fork之后产生的子进程和原来的父进程都会从fork后面的一条指定开始执行。唯一的区别就是,在父进程中,fork会返回子进程的pid。而在子进程中,fork会返回0。fork的原理参见此篇文章:
https://blog.csdn.net/ww1473345713/article/details/51708003
一般调用fork之后,有一个进程会调用exec,将本进程的内存空间替换成一个其他程序。
在windows系统中,创建新进程的函数是CreateProcess()。不同于fork,直接继承父进程的所有信息,CreateProcess()在创建进程的时候必须指明要加载的程序。而且CreateProcess()需要输入多达十个参数。
前趋图
表明进程之间的执行关系,或者说依赖关系。
地址空间
1)子进程复制父进程的地址空间
2)子进程重新装入一个程序
3.3.2 process termination
当进程结束运行,会调用exit()这个系统调用,并向父进程返回状态。操作系统会回收所有的资源。
但是直到其父进程调用wait函数之前,此进程的条目都必须存在,因为必须将退出状态保存下来。已经结束,但是父进程没有调用wait函数的,叫做僵尸进程。如果父进程在调用wait之前就已经退出了,这会导致子进程成为孤独进程。unix的解决办法是将init进程指定为孤儿进程的父进程,然后init进程会周期性的调用wait。
3.4 interprocess communciation
进程有很多进行互相通信的目的,如信息共享,计算加速(多核cpu),模块化。
信息共享的两种方式是内存共享和信息传递。一般来说共享内存更快,但是消息传递实现起来更简单。
对于多核cpu的研究表明,消息传递反而更快,这是因为在多核cpu中要同步多个不同的缓存,这很大程序上影响了共享内存的效率。
3.4.1 shared-memory systems
没啥可记的,就是多个进程要解除操作系统对于不能访问其他进程地址空间的限制。然后对同一块内存进行读写操作从而实现信息交换。如何解决同步的问题第五章讲解。
3.4.2 message-passing systms
简单的概念,就是tcp/udp那一套
3.5 example of IPC systems
3.5.1 posix shared memory
posix:portable operating system interface of unix。为了实现源码级别的可移植性,规定了一系列的接口标准。
利用posix提供的接口实现共享内存通信的示例:
#include <stdio.h>
#include <stlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
int main()
{
/* the size (in bytes) of shared memory object */
const int SIZE 4096;
/* name of the shared memory object */
const char *name = "OS";
/* strings written to shared memory */
const char *message 0 = "Hello";
const char *message 1 = "World!";
/* shared memory file descriptor */
int shm fd;
/* pointer to shared memory obect */
void *ptr;
/* create the shared memory object */
shm fd = shm open(name, O CREAT | O RDRW, 0666);
/* configure the size of the shared memory object */
ftruncate(shm fd, SIZE);
/* memory map the shared memory object */
ptr = mmap(0, SIZE, PROT WRITE, MAP SHARED, shm fd, 0);
/* write to the shared memory object */
sprintf(ptr,"%s",message 0);
ptr += strlen(message 0);
sprintf(ptr,"%s",message 1);
ptr += strlen(message 1);
return 0;
}
Figure 3.17 Producer process illustrating POSIX shared-memory AP
消费者的代码类似,只是由写变成了读:
/* read from the shared memory object */
printf("%s",(char *)ptr);
可以看出,使用共享内存进行通信大致分为三步
创建内存共享对象,并设定对象大小
将对象映射到内存
向对象写入或读取数据
3.5.2 3.5.3略
3.6 communciation in client-server systems
3.6.1 socket
太熟悉了,略
3.6.2 remote procedure call
为了实现在通过网络互联的系统之间实现的函数调用。RPC之间传递的消息具有良好的结构,指明要调用哪个函数,传递哪些参数,并如何将结果全部返回。
Typically, a separate stub exists for each separate remote procedure.stub会完成与远端系统的交互。使应用程序不感知这是一个remote procedure call。
为了解决不同系统对数据的表示格式不同的问题,定义了一种新的数据格式:external data representation (XDR)。在通过网络进行交互的时候,都需要进行一次转换。
另一个问题是rpc可能由于网络问题而导致失败,重复。解决办法是由操作系统来确保程序调用被准确的执行一次。解决超过一次的办法是为每个调用设置时间戳,接收方记录时间戳,遇到执行过的便丢弃掉。解决可能一次也没执行的办法就是由接收方对消息进行确认。
还有一个问题是客户端怎么知道服务端提供服务的端口。一种是固定写死。一种是专门提供一个查询端口,查询其他服务的端口。
3.6.3 pipes
oridinary pipe
标准的生产者消费者模式,一端写入,另一端读出,所以是单向通信。
在unix中,创建管理的函数:
pipe(int fd[])
fd[0] is the read-end of the pipe, and fd[1] is the write-end。
pipe被当做一种特殊的文件,所以可以通过文件的方式对pipe进行操作。
一个普通管道在进程外部无法被获取。管道通常被用来在父进程和子进程之间通信:创建pipe,然后通过fork创建子进程。子进程从父进程处继承pipe,然后进行通信。
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define BUFFER SIZE 25
#define READ END 0
#define WRITE END 1
int main(void)
{
char write msg[BUFFER SIZE] = "Greetings";
char read msg[BUFFER SIZE];
int fd[2];
pid t pid;
/* create the pipe */
if (pipe(fd) == -1)
{
fprintf(stderr, "Pipe failed");
return(1);
}
/* fork a child process */
pid = fork();
if (pid < 0) /* error occurred */
{
fprintf(stderr, "Fork Failed");
return(1);
}
if (pid > 0) /* parent process */
{ /* close the unused end of the pipe */
close(fd[READ END]);
/* write to the pipe */
write(fd[WRITE END], write msg, strlen(write msg) + 1);
/* close the write end of the pipe */
close(fd[WRITE END]);
}
else { /* child process */
/* close the unused end of the pipe */
close(fd[WRITE END]);
/* read from the pipe */
read(fd[READ END], read msg, BUFFER SIZE);
printf("read %s", read msg);
/* close the write end of the pipe */
close(fd[READ END]);
}
return(0);
}
不太明白的一点是,为什么父进程和子进程要先调用close(fd[READ END]);close(fd[WRITE END]);
而且这不会导致整个管道被关闭。
named pipes
once
the processes have finished communicating and have terminated, the ordinary pipe ceases to exist. Named pipe’s communciation can be bidirectional.Once a named pipe is established, several processes can use it for communication. named pipes continue to exist after communicating processes have finished.