相关概念
1. 进程与程序
程序:死的。只占用磁盘空间。 ——剧本。
进程;活的。运行起来的程序。占用内存、cpu等系统资源。 ——戏。
2. 并发
并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。
但,任一个时刻点上仍只有一个进程在运行。
例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做
一个进程的话,为什么可以同时运行呢,因为并发。
3. 虚拟内存和物理内存
虚拟内存:0~4G, 每个进程都可以访问这么多逻辑地址
物理内存:实际的内存大小
虚拟内存到物理内存的映射:通过地址变换机制(MMU)---查页表(页号--物理块号)。页表是程序调入内存时操作系统
填充的
实现虚拟内存存储器要实现的功能:
硬件支持(页表机制)
请求调入功能---页表项增加状态位:该页是否调入内存
置换功能----页表项增加访问字段:记录一段时间被访问的次数,使用置换算法
缺页中断机制
地址变换机制
3.1 一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了 4G。而用到的内存区
域,会通过页表映射到物理内存。
3.2 每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是
不同的。内核用的是 3G 以上的 1G 虚拟内存地址,
3.3 虚拟地址的 0-3G 对于一个进程的用户态和内核态来说是可以访问的,而 3-4G 是只有进程的内核
态可以访问的
3.4 所谓“独立拥有的虚拟地址”是指对于每一个进程,都可以访问自己的 0-4G 的
虚拟地址。虚拟地址是“虚拟”的,需要转化为“真实”的物理地址。
4. PCB进程控制块:
进程id
文件描述符表
虚拟内存相关(页表)
进程状态: 初始态、就绪态、运行态、挂起态、终止态。
进程工作目录位置
*umask掩码
信号相关信息资源。
用户id和组id
5. 环境变量:是指在操作系统中用来指定操作系统运行环境的一些参数。环境变量定义了进程的运行环境,环境变量类似
于全局变量,可以被各个进程访问到。我们可以通过修改环境变量来方便地修改系统配置。
5.1 PATH:可执行文件(命令)的存储路径。路径与路径之间用:分隔。当某个可执行文件同时出现在多个路径中时,会选择从左到右数第一个路径中的执行。下列所有存储路径的环境变量,均采用从左到右的优先顺序。
5.2 SHELL:当前 Shell,它的值通常是/bin/bash。
5.3 LD_LIBRARY_PATH:用于指定动态链接库(.so文件)的路径,其内容是以冒号分隔的路径列表。
5.4 C_INCLUDE_PATH:C语言的头文件路径,内容是以冒号分隔的路径列表。
5.5 CPLUS_INCLUDE_PATH:CPP的头文件路径,内容是以冒号分隔的路径列表。
5.6 HOME:用户的家目录。
进程控制
fork函数:
pid_t fork(void)
创建子进程。父子进程各自返回。父进程返回子进程pid。 子进程返回 0,表示子进程被创建出来了
fork()函数调用,fork()之下的代码会被调用两次(自己和儿子各调用一次,各自返回)
循环创建n个子进程:由于fork()一次,进程树上每一个节点都会创建子进程,若循环n次,会创建2^n - 1
个进程
所以循环n次,每次循环不让子进程参与fork(),即fork()==0时,表示子进程的执行,break;
getpid();getppid();循环创建N个子进程模型。 每个子进程标识自己的身份。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i;
pid_t pid;
for (i = 0; i < 3; i++){
pid = fork();
if (pid == 0){
break;
}
}
if (i == 3){
sleep(i);
printf ("I am parent\n");
}else{
sleep(i);
printf("I am %d child\n", i);
}
}
父子进程相同:
刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式
父子进程不同:
进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集
似乎,子进程复制了父进程 0-3G 用户空间内容,以及父进程的 PCB,但 pid 不同。真
的每 fork 一个子进程都要将父进程的 0-3G 地址空间完全拷贝一份,然后在映射至物理内存
吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进
程的逻辑还是执行自己的逻辑都能节省内存开销。
全局变量是否共享:不共享!!!
遵循 读时共享、写时复制的原则,即父进程的全局变量var=100.
1. 在父进程中更改:var=288. 这是将全局变量拷贝一份,再进行修改。此时内存里有两份var
2. 在子进程中打印:var=100. 这是将原来的全局变量打印出来
注意局部变量:fork()函数执行后,原来的局部变量拷贝一份给子进程,同一个局部变量要分清是父进程的还是子进程的
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int i;
pid_t pid; int var = 100;
pid = fork();
if (pid == -1){
perror("fork error");
exit(1);
}else if (pid == 0){
var = 288;
printf ("child's var = %d\n", var);
}else{
sleep(1);
printf ("father's var = %d\n", var);
}
return 0;
}
父子进程共享:
读时共享、写时复制。———————— 全局变量。
- 文件描述符 2. mmap映射区。
父子进程gdb调试
由于fork()之后的代码,父子进程都会执行一次,gdb需要选择跟踪调试父进程还是子进程
gdb调试:
设置父进程调试路径:set follow-fork-mode parent (默认)
设置子进程调试路径:set follow-fork-mode child
exec函数族
exec函数族:
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子
进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的
用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创
建新进程,所以调用 exec 前后该进程的 id 并未改变。
将当前进程的.text、.data 替换为所要加载的程序的.text、.data,然后让进程从新的.text
第一条指令开始执行,但进程 ID 不变,换核不换壳。
使进程执行某一程序。成功无返回值,失败返回 -1
int execlp(const char *file, const char *arg, ...); 借助 PATH 环境变量找寻待执行程序
参1: 程序名
参2: argv0
参3: argv1
...: argvN
哨兵:NULL
int execl(const char *path, const char *arg, ...); 自己指定待执行程序路径。
一般规律:
exec 函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通
常我们直接在 exec 函数调用后直接调用 perror()和 exit(),无需 if 判断。
练习:实现ps aux
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int fd = open("ps.out", O_WRONLY|O_CREAT|O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
return 0;
}
回收子进程
孤儿进程:
父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程领养。
僵尸进程:
子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。 kill 对其无效。
wait函数: 回收子进程退出资源, 阻塞回收任意一个。
pid_t wait(int *status)
参数:(传出) 回收进程的状态。
返回值:成功: 回收进程的pid
失败: -1, errno
函数作用1: 阻塞等待子进程退出
函数作用2: 清理子进程残留在内核的 pcb 资源
函数作用3: 通过传出参数,得到子进程结束状态
获取子进程正常终止值:
WIFEXITED(status) --》 为真 --》调用 WEXITSTATUS(status) --》 得到 子进程 退出值。
获取导致子进程异常终止信号:
WIFSIGNALED(status) --》 为真 --》调用 WTERMSIG(status) --》 得到 导致子进程异常终止的信号编号。
练习:wait()函数测试
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
int status;
pid_t pid;
pid = fork();
if (pid == -1){
perror("fork() error");
exit(1);
}else if (pid == 0){
execl("./abnor", "abnor", NULL);
perror("abnor error");
exit(2);
}else{
int wpid = wait(&status); //传出参数
if(WIFEXITED(status)){//正常退出
printf("I'm parent, The child process "
"%d exit normally\n", wpid);
printf("return value:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {//异常退出
printf("The child process exit abnormally, "
"killed by signal %d\n", WTERMSIG(status));
//获取信号编号
} else {
printf("other...\n");
}
}
return 0;
}
waitpid函数: 指定某一个进程进行回收。可以设置非阻塞。 waitpid(-1, &status, 0) == wait(&status);
pid_t waitpid(pid_t pid, int *status, int options)
参数:
pid:指定回收某一个子进程pid
> 0: 待回收的子进程pid
-1:任意子进程
0:同组的子进程。
< -1 回收指定进程组内的任意子进程
status:(传出) 回收进程的状态。
options:WNOHANG 指定回收方式为,非阻塞。
返回值:
> 0 : 表成功回收的子进程 pid
0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。
-1: 失败。errno
练习:waitpid()函数测试:删除指定进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
pid_t pid, tmppid, wpid;
int i = 0;
for (; i < 5; i++){
pid = fork();
if (pid == 0){
break;
}
if (i == 2){
tmppid = pid;
}
}
if (i == 5){
printf ("tmppid = %d\n", tmppid);
wpid = waitpid(tmppid, NULL, 0);
printf ("my child pid is %d\n", wpid);
}else{
sleep(i);
printf ("I am %d child,pid = %d\n", i + 1, getpid());
}
}
练习:wait、waitpid 一次调用,回收一个子进程。想回收多个。while
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int i;
pid_t pid, wpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) { // 循环期间, 子进程不 fork
break;
}
}
if (5 == i) { // 父进程, 从 表达式 2 跳出
/*
while ((wpid = waitpid(-1, NULL, 0))) { // 使用阻塞方式回收子进程
printf("wait child %d \n", wpid);
}
*/
while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) { //使用非阻塞方式,回收子进程.
if (wpid > 0) {
printf("wait child %d \n", wpid);
} else if (wpid == 0) {
sleep(1);
continue;
}
}
} else { // 子进程, 从 break 跳出
sleep(i);
printf("I'm %dth child, pid= %d\n", i+1, getpid());
}
return 0;
}
总结:wait(), waitpid()在父进程中调用,回收子进程