进程
进程
进程的概念:执行中的程序。
进程是现代分时系统的工作单元。
系统由一组进程组成:操作系统进程执行系统代码,用户进程执行用户代码。通过 CPU 多路复用,所有的进程可以并发执行。通过进程之间的切换,操作系统能使计算机更为高效。
进程概念
进程
进程是执行中的程序,这样的说法显得有点非正式。进程不只是程序代码,程序代码被称为文本段
(或代码段)。进程还包括当前活动,通过程序计数器的值和处理器寄存器的内容来表示。另外,进程还包括进程栈
、堆
和数据段
。
在内存中的进程如下:
进程状态
每个进程都处于下列状态之一:
1.new:进程刚进入内存。
2.running:进程正被执行。
3.wait:进程等待某个时间的发生(如完成 I/O 或收到信号)。
4.ready:进程等待分配 CPU。
5.terminated:进程已完成执行。
进程会在这些状态之间切换,状态切换图如下:
创建(new)
执行(running)
阻塞(wait)
就绪(ready)
退出(terminated)
进程控制块
每个进程在操作系统内都有对应的进程控制块(process control block,PCB)。它包含许多与一个特定进程相关的信息。
PCB | 作用 |
---|---|
进程状态 | 记录进程状态 |
进程编号 | 每个进程都有一个特定的编号来标记自己,该编号是唯一的 |
程序计数器 | 计数器表示进程要执行的下个指令的地址 |
寄存器 | 根据计算机体系结构的不同,寄存器的数量和类型也不同。包括累加器、索引寄存器、堆栈指针、通用寄存器和其他条件码信息寄存器。与程序寄存器一起,这些状态在出现中断时也需要保存,以便以后能正确地执行 |
内存界限 | 进程可以访问的内存范围 |
打开文件列表 | 记录进程打开过的文件 |
… | … |
进程调度
多道程序设计的目的是无论何时都有进程在运行,从而使 CPU 利用率达到最大化。分时系统使在进程之间快速切换 CPU 以便用户在程序运行时能与其进行交互。
为了达到上述目的,需要设计进程调度,来决定哪个进程进入到内存,CPU 执行何种进程。
调度队列
进程进入内存时,会被加到作业队列
中,该队列包括系统中的所有进程。
驻留在内存中就绪的、等待运行的进程保存在就绪队列
中。该队列通常用链表来实现,其头节点指向第一个和最后一个 PCB 块的指针。每个 PCB 包括一个指向就绪队列的下一个 PCB 的指针域。
等待特定 I/O 设备的进程列表称为设备队列
。每个设备都有自己的设备队列。
新进程开始处于就绪队列。它在就绪队列中等待直到被选中执行或被派遣。当进程分配到 CPU 并执行时,可能发生下面几种事件的一种:
1.进程发出 I/O 请求,并被放到 I/O 队列中(设备队列)。
2.进程可能创建一个新的子进程,并等待其结束。
3.进程可能会由于中断而强制释放 CPU,并被放回到就绪队列。
对于前两种情况,进程最终从等待状态切换到就绪状态,并放回到就绪队列中。进程继续这一循环直到终止,倒是它将从所有队列中删除,其 PCB 和资源将得以释放。
调度程序
进程在其生命周期中会在各种调度队列之间迁移。为了调度,操作系统必须按照某种方式从这些队列中选择进程。进程选择是由相应的调度程序
来执行的。
总共有三种类型的调度程序:
1.长期调度程序
。对于批处理程序,被提交但没有进入内存的进程被放到大容量存储设备(通常为磁盘)的缓冲池中,保存在那里以便以后执行。长期调度程序从该池选择进程,并装入到内存以准备执行。
2.短期调度程序
。从就绪队列中选择程序,并为之分配 CPU。
3.中期调度程序
。中期调度的核心思想是能将进程从内存(或从 CPU 竞争)中移出,从而降低多道程序设计的程度。之后,进程能被重新调入内存,并从中断处继续执行。这种方案称之为交换
(swapping)。
长期调度程序和短期调度程序的主要差别是它们执行的频率。短期调度程序必须频繁地为 CPU 选择新进程。进程可能执行数毫秒(ms)就会进行 I/O 请求,短期调度程序通常每100ms至少执行一次。由于每次执行之间的时间较短,短期调度程序必须要快。如果需要10ms(进程调度所花的时间)来确定执行一个运行100ms的进程,那么 10/(100 + 10) 的 CPU 时间会用于(或浪费在)调度工作。
长期调度程序执行得并不频繁,在系统内新进程的创建之间可能有数分钟间隔。长期调度程序控制多道程序设计的程度
(内存中的进程数量)。如果多道程序的程度稳定,那么创建进程的平均速度必须等于进程离开系统的平均速度。因此,只有当进程离开系统后,才可能需要长期调度程序。由于每次执行之间时间间隔得较长,长期调度程序能使用更多的时间来选择进程。
绝大多数进程可分为:I/O 为主或 CPU 为主。长期调度程序应该选择一个合理的包含 I/O 为主的和 CPU 为主的组合进程。
上下文切换
中断使 CPU 从当前任务改变为运行内核子程序,这样的操作在通用系统中发生的很频繁。
当发生一个中断时,系统需要保存当前运行进程的上下文,从而能够在之后重新运行。进程的上下文用进程的 PCB 表示。
当发生上下文切换时,内核会将旧进程的状态保存在其 PCB 中,然后装入经调度要执行的并已保存的新进程的上下文。
进程操作
绝大多数系统内进程能并发执行,他们可以动态创建和删除。
进程创建
能通过创建进程系统调用(create-process system call)创建多个新进程。创建进程的进程称为父进程,被创建的进程称为子进程。
每个进程都可以创建新进程,而所有的进程都有一个相同的祖先,这就形成了进程树。
大多数操作系统(包括 UNIX 和 Windows 系列操作系统)根据一个唯一的进程标识符(process identifier,pid)来标志进程,pid
通常是一个整数值。
通常,进程需要一定的资源(如 CPU 时间、内存、文件、I/O 系统)来完成任务。在一个进程创建进程时,子进程可能从操作系统那里直接获得资源,也可能只从其父进程那里获得资源。进程创建时,除了各种物理和逻辑资源之外,还需要初始化数据
(输入,在 Windows 上体现在点击可执行文件时,会将文件路径输入到新进程中),这部分由父进程传递给子进程。
当进程创建新进程时,有两种后续可能:
1.父进程和子进程并发执行。
2.父进程等待,直到某个子进程或全部子进程执行完毕。
新进程的地址空间也有两种可能:
1.子进程是父进程的复制品
2.子进程装入另一个新程序
Unix 系列操作系统创建子进程:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 1;
pid_t pid;
// vfork和fork有所区别,这里调用vfork更节省资源
pid = vfork();
if(pid < 0)
exit(1);
else if(pid == 0)
{
printf("i: %d\n", i++);
printf("Child process: %d\n", getpid());
execlp("ls", "ls", "-l", NULL);
exit(1);
}
else
{
// vfork 必定阻塞父进程,因此不需要wait或waitpid
// wait(NULL);
// waitpid(-1, NULL, NULL);
printf("i: %d\n", i++);
printf("Parent process: %d\n", getpid());
}
exit(0);
}
进程终止
当进程执行完最后的语句并使用系统调用 exit() 请求操作系统删除自身时,进程终止。这时,进程可以返回状态值到父进程(父进程通过调用 wait 或 waitpid 获得)。所有进程资源会被操作系统释放。
如果父进程终止,那么所有子进程会以 init 进程作为父进程。
进程间通信
操作系统内并发执行的进程可以是独立进程或协作进程。如果一个进程不能影响其他进程或被其他进程所影响,那么该进程是独立的;反之该进程是协作的。
协作进程需要一种进程间通信机制(interprocess communication,IPC)来允许进程相互交换数据和信息。
进程间通信有两种基本模式:
1.共享内存
2.消息传递
消息传递比共享内存更易于实现,但共享内存允许以最快的速度进行方便的通信。
共享内存
内核会在物理空间上开辟一个共享内存区域,进程可以指向该共享内存区域,然后读写数据。
/*
共享内存本身不提供同步机制,这意味着它不能保证当某个进程在往共享内存区域写东西的时候,其他进程不会读这个共享内存区域。
需要自己用信号量做好同步
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/shm.h>
int main()
{
int segment_id;
char* buf;
int buflen = 1024;
segment_id = shmget(IPC_PRIVATE, buflen, 0);
if(segment_id < 0)
exit(1);
buf = (char*)shmat(segment_id, NULL, 0);
pid_t pid = fork();
if(pid < 0)
exit(1);
else if(pid == 0)
{
snprintf(buf, buflen, "Hello World!");
shmdt(buf);
if(shmctl(segment_id, IPC_RMID, NULL) < 0)
exit(1);
exit(0);
}
else
{
wait(NULL);
printf("%s\n", buf);
shmdt(buf);
if(shmctl(segment_id, IPC_RMID, NULL) < 0)
exit(1);
exit(0);
}
}
消息传递
消息传递大致有以下5种方式:
1.半双工 Unix 管道
2.FIFOs(有名管道)
3.消息队列
4.信号量
5.网络socket
半双工 Unix 管道
该管道又称为无名管道,主要用于有血缘关系进程之间的通信。
主要函数为:
int pipe(int pipefd[2]); // 创建管道
pipefd[0] 为读端
pipefd[1] 为写端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
int pipefd[2];
if(pipe(pipefd) < 0)
exit(1);
pid_t pid = fork();
if(pid < 0)
exit(1);
else if(pid == 0)
{
if(close(pipefd[0]) < 0)
exit(1);
char msg[13] = "Hello World!";
char* ptr = msg;
int msglen = strlen(msg);
int n;
while(msglen)
{
if((n = write(pipefd[1], ptr, msglen)) < 0)
exit(1);
else
{
ptr += n;
msglen -= n;
}
}
if(close(pipefd[1]) < 0)
exit(1);
exit(0);
}
else
{
wait(NULL);
if(close(pipefd[1]) < 0)
exit(1);
char buf[1024];
memset(buf, '\0', 1024);
char* ptr = buf;
int n;
int buflen = 1024;
while(1)
{
if((n = read(pipefd[0], ptr, buflen)) == 0)
break;
else if(n < 0)
exit(1);
else
{
ptr += n;
buflen -= n;
}
}
printf("%s\n", buf);
if(close(pipefd[0]) < 0)
exit(1);
exit(0);
}
}
FIFOs(有名管道)
有名管道是一个设备,以 FIFO 的文件形式存储于文件系统中。因此,即使进程之间没有血缘关系,也可以通过有名管道通信。
主要函数为:
int mkfifo(const char* pathname, mode_t mode);
int mknod(const char* pathname, mode_t mode, dev_t dev);
创建完之后,对于有名管道文件的读写操作和对普通文件的读写操作相似,这里不测试代码了。
与一般文件稍微有些不同的是,调用 open 打开有名管道文件会有阻塞的情况。只有当读端和写端都被打开时,才不阻塞。
消息队列
消息队列本质是由内核创建的用于存放消息的链表。进程通过共享操作同一个
消息队列,实现进程间通信。每一个消息队列都有唯一的标识符
。
每个消息由两部分组成:
1.消息编号,用于识别消息。
2.消息正文,存储信息内容
创建(获得)消息队列
通过函数 msgget 实现。
函数 msgget 如下:
int msgget(key_t key, int msgflg);
// key, 用于生成消息队列的标识符
// msgflg,指定创建时的原始权限,比如0666。除了原始权限,还需要指定 IPC_CREAT选项。msg_id = msgget(key, 0666 | IPC_CREAT);
// 成功返回消息队列标识符,失败返回负数
发送消息
进程先封装一个消息包,消息包的结构如下:
struct msgbuf
{
long mtype; // 消息编号,> 0
char mtext[msgsz]; // 消息正文
};
然后再调用 msgsnd 发送消息到消息队列上。
msgsnd 函数如下:
int msgsnd(int msg_id, const void* msgp, size_t msgsize, int msgflg);
// msg_id,消息队列标识符
// msgp,消息包
// msgsize,消息正文的大小
// msgflg, 0的话为阻塞;IPC_NOWAIT的话为非阻塞
// 成功返回0,失败返回负数
接受消息
从消息队列上接受消息,通过函数 msgrcv 实现。
函数 msgrcv 如下:
ssize_t msgrcv(int msg_id, void* msgp, size_t msgsize, long msgtyp, int msgflg);
// msg_id,消息队列的标识符
// msgp,用来存放消息包的地址
// msgsize,消息正文的大小
// msgtyp,需要接受的消息的消息编号
// msgflg, 为0的话阻塞;IPC_NOWAIT的话非阻塞
// 成功返回消息正文的字节数,失败返回负数
释放消息队列
通过函数 msgctl 实现。
函数 msgctl 如下:
int msgctl(int msg_id, int cmd, struct msqgid_ds* buf);
// msg_id,消息队列的标识符
// cmd,控制选项
// buf,存放属性信息
// 成功返回0,失败返回负数
代码例子
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <string.h>
#define MSGFILE "./msgfile"
int main()
{
key_t key;
key = ftok(MSGFILE, 'a');
int fd;
if((fd = msgget(key, 0666 | IPC_CREAT)) < 0)
exit(1);
struct msgbuf buf;
memset(&buf, 0, sizeof(buf));
buf.mtype = 1;
strncpy(buf.mtext, "Hello World!", 12);
if(msgsnd(fd, &buf, 1024, 0) < 0)
exit(1);
ssize_t msgsize;
struct msgbuf rbuf;
memset(&rbuf, 0, sizeof(rbuf));
if((msgsize = msgrcv(fd, &rbuf, 1024, 1, 0)) < 0)
exit(1);
printf("%s\n", rbuf.mtext);
if(msgctl(fd, IPC_RMID, NULL) < 0)
exit(1);
exit(0);
}
信号量
信号量本质上是一个计数器,用于多进程对共享数据对象的读取。与管道和消息队列不同,信号量不以传送数据为主要目的,而是用来保护共享资源(包括硬件资源)。
工作原理
有两个操作 P(sv) 和 V(sv),它们的作用如下:
- P(sv),如果 sv 的值大于零,就减一;如果 sv 的值为零,就挂起进程。
- V(sv),如果有其他继承因等待 sv 而挂起,就让它恢复执行;如果没有,就给 sv 加一。
PV操作都为原子操作。
二元信号量
二元信号量(Binary Semaphore),也称为互斥锁。它只有两个计数,1 和 0,分别代表非占用和占用。
创建信号量
函数 semget 用来创建信号量,如下:
int semget(key_t key, int nsems, int semflg);
// key, 用来生成信号量集标识符(同消息队列的key值一样,通常由 ftok 函数生成)
// nsems,指定信号量集中需要的信号量数目,它的值几乎总是1
// semflag,同消息队列一样,一般是 IPC_CREAT 与文件权限做或操作
删除信号量
函数 semctl 用来删除信号量,如下:
int semctl(int sem_id, int semnum, int cmd, ...);
// sem_id,信号量集标识符
// semnum,信号量在信号集里的编号
// cmd,控制位,同消息队列一样,IPC_RMID用来删除信号量
信号量操作
函数 semop 用来操作信号量,如下:
int semop(int sem_id, struct sembuf* sops, size_t nops);
// sem_id, 信号量集标识符
// nops: 操作信号量的个数,也就是结构数组sops里结构的数量
// sembuf 结构如下
struct sembuf
{
short sem_num; // 信号量在信号集里的编号
short sem_op; // 信号量的操作,-1为P,+1为V(也可以为其他值,表示对信号量的改变量)
short sem_flg; // 控制位,0阻塞;IPC_NOWAIT不阻塞;SEM_UNDO,程序结束时,保证信号值会被重设为semop调用前的值,避免程序异常结束对信号量的影响。
};
代码例子
/*
删除信号量会导致另一进程对信号量的操作出现错误,
不过本代码是为了直观表现对于二元信号量,PV操作都是成对出现的。
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <string.h>
#include <signal.h>
#define SEMFILE "./semfile"
typedef void (* Sigfunc) (int);
void signal_handler(int signo)
{
printf("signal_handler\n");
wait(NULL);
}
int main()
{
Sigfunc handler = signal(SIGCHLD, signal_handler);
key_t key = ftok(SEMFILE, 'a');
int fd;
if((fd = semget(key, 1, IPC_CREAT | 0666)) < 0)
exit(1);
struct sembuf buf;
memset(&buf, 0, sizeof(buf));
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
if(semop(fd, &buf, 1) < 0)
exit(1);
pid_t pid = fork();
if(pid < 0)
exit(1);
else if(pid == 0)
{
buf.sem_op = -1;
if(semop(fd, &buf, 1) < 0)
exit(1);
printf("Child process P\n");
buf.sem_op = 1;
if(semop(fd, &buf, 1) < 0)
exit(1);
printf("Child process V\n");
sleep(10);
if(semctl(fd, 0, IPC_RMID) < 0)
exit(1);
printf("Child process ctl\n");
exit(0);
}
else
{
buf.sem_op = -1;
if(semop(fd, &buf, 1) < 0)
exit(1);
printf("Parent process P\n");
buf.sem_op = 1;
if(semop(fd, &buf, 1) < 0)
exit(1);
printf("Parent process V\n");
sleep(10);
if(semctl(fd, 0, IPC_RMID) < 0)
exit(1);
printf("Parent process ctl\n");
exit(0);
}
}
网络socket
网络socket 应该是最常见的通信方式了(?可能对我来说是的)。除了在本地进程间通信,也可以在端与端的进程间通信。这边就不展开讲了,可以看链接: Tcp Daytime获取客户端 简单了解一下。