UNIX中,进程既是一个独立拥有资源的基本单位,又是一个独立调度的基本单位。一个进程实体由 若干个区(段)组成,包括程序区、数据区、栈区、共享存储区等。每个区又分为若干页,每个进程 配置有唯一的进程控制块PCB,用于控制和管理进程。
PCB组成
PCB的数据结构如下:
1.进程表项(Process Table Entry),包括一些最常用的核心数据: 进程标识符PID、用户标识符UID、进程状态、事件描述符、进程和U区在内存或外存的地址、 软中断信号、计时域、进程的大小、偏置值nice、指向就绪队列中下一个PCB的指针P_Link、 指向U区进程正文、数据及栈在内存区域的指针。
2.U区(U Area):用于存放进程表项的一些扩充信息。 每一个进程都有一个私用的U区,其中含有:进程表项指针、真正用户标识符u-ruid(real user ID)、有效用户标识符u-euid(effective user ID)、用户文件描述符表、计时器、内部I/O参数、限制字段、差错字段、返回值、信号处理数组。 由于UNIX系统采用段页式存储管理,为了把段的起始虚地址变换为段在系统中的物理地址,便于实现区的共享,所以还有:
3.系统区表项:以存放各个段在物理存储器中的位置等信息。 系统把一个进程的虚地址空间划分为若干个连续的逻辑区,有正文区、数据区、栈区等。这些区 是可被共享和保护的独立实体,多个进程可共享一个区。为了对区进行管理,核心中设置一个系 统区表,各表项中记录了以下有关描述活动区的信息: 区的类型和大小、区的状态、区在物理 存储器中的位置、引用计数、指向文件索引结点的指针。
4.进程区表:系统为每个进程配置了一张进程区表。表中,每一项记录一个区的起始虚地址及指向系统区表中 对应的区表项。核心通过查找进程区表和系统区表,便可将区的逻辑地址变换为物理地址。
进程映像
UNIX系统中,进程是进程映像的执行过程,也就是正在执行的进程实体。它由三部分组成:
- 用户级上、下文。主要成分是用户程序;
- 寄存器上、下文。由CPU中的一些寄存器的内容组成,如PC,PSW,SP及通用寄存器等;
- 系统级上、下文。包括OS为管理进程所用的信息,有静态和动态之分。
fork(创建)
创建一个新进程。
int fork();
如果fork
调用成功,其会:
- 向父进程返回子进程的PID
- 向子进程返回0,表示当前进程为子进程
即fork()
被调用一次,但返回了两次。
此时OS在内存中建立一个新进程,所建的新进程是调用fork
父进程的副本,称为子进程。子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文,两者并发执行。
#include <stdio.h>
#include <unistd.h>
main() {
int p1, p2, i;
p1 = fork(); //创建子进程p1
if (p1 > 0) { //父进程
p2 = fork(); //创建子进程p2
if (p2 > 0)
for (i = 0; i < 10; i++) printf("parent %d\n", i);
else
for (i = 0; i < 10; i++) printf("son %d\n", i);
} else {
for (i = 0; i < 10; i++) printf("daughter %d\n", i);
}
}
核心为 fork() 完成以下操作:
1.为新进程分配一进程表项和进程标识符 进入 fork()
后,核心检查系统是否有足够的资源来建立一个新进程。若资源不足,则 fork()
系统调用失败;否则,核心为新进程分配一进程表项和唯一的进程标识符。
2.检查同时运行的进程数目超过预先规定的最大数目时,fork()
系统调用失败。
3.拷贝进程表项中的数据将父进程的当前目录和所有已打开的数据拷贝到子进程表项中,并置进程的状态为“创建”状态。
4.子进程继承父进程的所有文件 对父进程当前目录和所有已打开的文件表项中的引用计数加1。
5.为子进程创建进程上、下文 进程创建结束,设子进程状态为“内存中就绪”并返回子进程的标识符。
6.子进程执行 虽然父进程与子进程程序完全相同,但每个进程都有自己的程序计数器PC(注意子进程的PC开始 位置),然后根据pid变量保存的 fork()
返回值的不同,执行了不同的分支语句。
sleep(阻塞)
使当前的进程睡眠,即当前的进程进入阻塞态。
unsigned int sleep(unsigned int seconds);
#include <stdio.h>
main() {
printf("I'm sleeping...\n");
sleep(10000);
}
wait(挂起)
wait()
将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait()
前已有子进程暂停或终止,则调用进程做适当处理后便返回。
int wait(int *status);
status
是用户空间的地址,一共16位。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8位则带回了exit()的返回值,exit()返回值由系统给出。
"0000 0001 0000 0002"中,0000 0001 就是高八位,0000 0002就是低八位。
核心对 wait()
作以下处理:
(1)首先查找调用进程是否有子进程,若无则返回出错码;
(2)若找到一处于“僵死状态”的子进程,则将子进程的执行时间加到父进程的执行时间上,并释放子进程的进程表项;
(3)若未找到处于“僵死状态”的子进程,则调用进程便在可被中断的优先级上睡眠,等待其子进程发来软中断信号时被唤醒。
exit(终止)
为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit()
来实现进程的 自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit()
,使子进程自我终止。
void exit(int status);
status
是返回给父进程的一个整数,exit(0)
表示进程正常终止,exit(1)
表示进程运行有错,异常终止。
如果调用进程在执行exit()时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main() {
int pid = fork(); //创建子进程
if (pid > 0) {
int status;
wait(&status);
printf("child process completed:%d (%d,%d)\n",status,status/256,status%256);
} else {
printf("Hello child!\n");
exit(2);
}
}
核心须为exit()
完成以下操作:
(1)关闭软中断 (2)回收资源 (3)写记帐信息 (4)置进程为“僵死状态”
进程装入
在进程的创建上 Unix 采用了一个独特的方法,它将进程创建与加载一个新进程映象二者分离。这样的好处是有更多的余地对两种操作进行管理。
当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用 exec 系列的函数来进行。当然,exec 系列的函数也可以将当前进程替换掉。
exec()系列
exec()
系列中的系统调用把一个新程序装入内存,来改变调用进程的执行代码,从而形成新进程。调用成功后,调用进程将被覆盖,然后从新程序的入口开始执行,这样就产生了一个新进程,新进程的进程标识符id与调用进程相同。
exec()
系列系统调用在UNIX系统库unistd.h中,有execl、execlp、execle、execv、**execvp **五个,其基本功能相同,只是以不同的方式来给出参数。
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
-
path
:表示你要启动程序的名称,包括路径名 -
arg
:表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束 -
return
:成功返回 0,失败返回 -1
exec和fork的联合
系统调用exec
和fork()
联合使用能为程序开发提供有力支持。用fork()建立子进程,然后在子进程中使用exec(),这样就实现了父进程与一个与它完全不同子进程的并发执行。
int status;
if (fork() == 0) {//创建子进程
execl(...);//替换子进程的执行文件
}
wait(&status);//父进程等待子进程结束
示例:
//hello.c
#include <stdio.h>
int main(int argc, char const *argv[]) {
int i;
for (i = 0; i < argc; i++) printf("arg %d : %s \n", i, argv[i]);
return 0;
}
//main.c
#include <stdio.h>
#include <unistd.h>
main() {
int pid = fork(); //创建子进程
if (pid > 0) {
wait(NULL);
printf("Child completed!\n");
} else {
execl("./hello", "hello", "a", "b", "c", NULL);
}
}
exec和fork的区别
exec没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程,所以exec调用成功后,没有任何数据返回,与fork不同。
-
fork()
:将父进程的用户级上下文拷贝到新进程中 -
exec()
:将一个可执行的二进制文件覆盖在调用进程的用户级上下文的存储空间上,以更改调用进程的用户级上下文。
spawn()系列
Windows平台进程的创建使用spawn()系列,创建并且执行一个新的进程,相等于fork() + exec()
;
spawn()
存放在process.h中。
int spawnl(int mode, const char *pathname, const char *arg0, const char *arg1, ...);
(Linux下相对应的函数名为posix_spawn(),存放在pthread.h中)
mode:
_P_WAIT: 同步调用
_P_NOWAIT: 异步调用
返回值:
mode 为 _P_WAIT 时,返回子进程结束返回值。
mode 为 _P_NOWAIT 时,返回子进程 ID。
示例:
//hello.c
#include <stdio.h>
int main(int argc, char **argv) {
int i;
for (int i = 0; i < argc; i++) {
printf("arg %d : %s \n", i, argv[i]);
}
return 0;
}
//main.c标签:fork,调用,const,int,编程,char,Linux,进程 From: https://blog.51cto.com/u_15936519/6047269
#inlcude<stdio.h>
#include<process.h>
main(){
int ret = spawnl(_P_WAIT,"hello.exe","hello","a","b","c",NULL);
printf("ret = %d \n",ret);
}