进程的相关概念
1.并发
2.单道程序设计
3.多道程序设计
4.cpu/mmu
5.进程控制块
6.进程状态
环境变量
1.常用的环境变量作用
2.函数
进程控制原语
1.fork函数 循环创建子进程的架构
2.exec函数 各个函数的参数使用方法/作用
3.wait/waitpid 回收子进程的一般方式
程序和进程
程序 是指编译号的二进制文件 例如a.out文件 保存在磁盘上 不占用系统资源(cpu、内存、打开的文件、设备、锁……)
进程 是一个抽象的概念 与操作系统原理联系紧密。进程是活跃的程序 占用系统资源 在内存中执行(程序运行起来 产生一个进程)
并发
并发在操作系统中 一个时间段中有多个进程都处于已经启动运行到运行完毕之间的状态 但任一个时刻点上仍只有一个进程在运行
单道程序设计模式:微软的DOS系统 同一时间cpu只能执行一个程序 下一个程序想要执行必须排队等第一个程序执行完毕 执行效率非常低
多道程序设计模式:可以看上去同时执行多个进程 但是实际上是做不到同时执行的 本质上是把每一个进程划分成多个任务片段 cpu把自己划分成多个时间轮片 然后把自己的时间轮片分配给其中的一个任务片段执行
时钟中断:是一种硬件手段 来划分cpu时间轮片的机制 当一个进程遇到时钟中断 是不可抗拒的放弃掉cpu来使得cpu执行其他进程
在多道程序设计模型中 多个进程轮流使用cpu。而当下常见cpu为纳秒级 1秒可以执行大约10亿条指令 由于人眼的反应速度是毫秒级 所以看似同时在运行
中央处理器的简易构架
cache:缓冲区
预取器每次只会从缓冲区中取一个指令
译码器的作用就是在分析这条指令在干嘛 需要哪些寄存器来配置完成运算
运算就交给ALU 这个算数逻辑单元 只会加法和左移 运算好了再返回给寄存器
mmu的基本工作原理
mmu位于cpu内部 作为一个硬件存在
虚拟地址:可用的地址空间有4G 真正占用的内存却没这么大
0x804a4000 int a = 10;
物理地址 1000
mmu帮助把虚拟地址和物理地址对应
cpu对于内存的访问级别分为3 2 1 0 其中3是最低的 0是最高的 Linux只使用了3级和0级
对照虚拟内存 用户空间是3级 内核空间是0级
进程彼此是独立的 同时运行两个a.out在物理内存上分别要分配物理内存空间 但是内核却只需要一份 两份同用一份内核空间
进程控制块PCB
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息 linux内核的进程控制块是task_struct结构体 在其内部成员有很多 重点掌握以下部分:
进程id。系统中每一个进程都有唯一的id 在c语言中用pid_t类型表示 其实就是一个非负整数
进程的状态,有就绪、运行、挂起、停止等状态
进程切换时需要保存和恢复的一些cpu寄存器
描述虚拟地址空间的信息
描述控制终端的信息
当前工作目录
umask掩码
文件描述符表 包含很多指向file结构体的指针
和信号相关的信息
用户id和组id
会话(session)和进程组
进程可以使用的资源上限(Resource Limit)
进程的状态:
环境变量简介
Linux操作系统是一个多任务多用户的开源操作系统
多任务:并发
多用户:同一时间点了可以有多个用户登陆到一台计算上
环境变量:是指在操作系统中用来指定操作系统运行环境的一些参数 通常有以下特征:
1.字符串(本质) 2.有统一的格式:名=值[:值] 3.值用来描述进程环境信息
PATH
用来记录文件的可执行路径 echo $PATH
SHELL
记录当前的命令解析器是什么 当前Shell 它的值通常是/bin/bash
TERM
当前终端类型 图形界面通常是xterm 终端类型决定了一些程序的输出显示方式 比如图形界面终端可以显示汉子 而字符终端一般不行
LANG
语言和locale 决定了字符编码以及时间、货币等信息的显示格式
HOME
当前用户主目录的路径 很多程序需要在主目录下保存配置文件 使得每个用户在运行该程序时都有自己的一套配置
查看进程环境变量
#include <stdio.h>
//当前进程下的环境变量表
extern char **environ;
int main(void){
for(int i=0;environ[i];i++){
printf("%s\n",environ[i]);
}
return 0;
}
编译运行后就能输出这个.c文件进程的环境变量表
环境变量操作函数
getenv函数
获取环境变量值
char *getenv(const char* name); 成功:返回环境变量的值 失败:NULL(name不存在)
setenv函数
设置环境变量的值
int setenv(const char* name, const char* value, int overwrite); 成功:0 失败:-1
参数overwrite取值:1:覆盖原环境变量
0:不覆盖 该参数常用于设置新环境变量 如 ABC=haha-day-night
unsetenv函数
删除环境变量name的定义
int unsetenv(const char *name); 成功:0 失败:-1
注意事项:name不存在仍返回0(成功) 当name命名为"ABC="时会出错
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void){
char *val;
const char *name = "ABD";
val = getenv(name);
printf("1.%s = %s\n",name,val);
setenv(name, "haha-day-night",1); //here 1 means add a new env
val = getenv(name);
printf("2.%s = %s\n",name,val);
#if 0
int ret = unsetenv("ABDFGH");
printf("ret = %d\n",ret);
val = getenv(name);
printf("3.%s = %s\n",name,val);
#else
int ret = unsetenv("ABD");
printf("ret = %d\n",ret);
val = getenv(name);
printf("3.%s = %s\n",name,val);
#endif
return 0;
}
输出结果:
ABD = (null) //说明当前环境变量中没有一个叫ABD的
ABD = haha-day-night //把ABD改名set成haha-day-night 再打印就有了
ret = 0 //0代表成功 成功删除ABD
ABD = (null) //再次获取ABD就变成NULL了
创建单个子进程
fork函数
创建一个子进程
pid_t fork(void); 失败返回-1 成功返回子进程的pid[非负整数]和0(两个返回值)
当父进程走到fork的时候就会创建出一个子进程 和父进程同时往下执行 这个时候就出现了两个返回值 父进程fork返回子进程id 子进程fork返回0
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void){
pid_t pid;
printf("xxxxxxxxxxxx\n");
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid == 0){
printf("i am child, pid = %u, ppid = %u\n",getpid(),getppid());
}else{
printf("i am parent, pid = %u, ppid = %u\n",getpid(),getppid());
sleep(1);
}
printf("YYYYYYYYYYYYYYY\n");
return 0;
}
输出结果:
xxxxxxxxxxxx
i am parent, pid = 16715, ppid = 2302
i am child, pid = 16716, ppid = 16715
YYYYYYYYYYYYYYY
这里parent的ppid 2302 通过ps aux | grep 2302查询一下就知道这是bash 说明bash作为这个程序的父进程执行了这个程序
循环创建N个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void){
pid_t pid;
printf("xxxxxxxxxxxx\n");
//循环创建5个子进程
for(int i=0;i<5;i++){
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid == 0){
printf("i am %dth child, pid = %u, ppid = %u\n",i+1,getpid(),getppid());
}else{
printf("i am %dth parent, pid = %u, ppid = %u\n",i+1,getpid(),getppid());
sleep(1);
}
}
printf("YYYYYYYYYYYYYYY\n");
return 0;
}
⚠️:但是输出发现并不是5个子进程
解决方法:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void){
int i;
pid_t pid;
printf("xxxxxxxxxxxx\n");
//循环创建5个子进程
for(i=0;i<5;i++){
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid == 0){
break; //子进程直接break
}
}
if(i<5){
sleep(i);
printf("i am %d child, pid = %u\n",i+1,getpid());
}else{
sleep(i);
printf("i am parent");
}
return 0;
}
输出结果:
xxxxxxxxxxxx
i am 1 child, pid = 17065
i am 2 child, pid = 17066
i am 3 child, pid = 17067
i am 4 child, pid = 17068
i am 5 child, pid = 17069
i am parent
总结:这里的系统运行流程其实是先做了五次循环生成了五个子进程 然后五个子进程和一个父进程一共六个进程同时开始抢占cpu 因为设置了sleep 所以可以控制了输出的顺序 让第一个子进程先输出 然后是最后一个子进程 最后才是父进程 因为父进程里是sleep了5秒
父子进程共享
父子进程之间在fork后 有哪些相同 哪些相异之处呢
刚fork过后:
父子相同处:全局变量、.data、.txt、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式……
父子不同之处:1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器)。 6.未决信号集
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int var = 34;
int main(void){
pid_t pid;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid >0){
sleep(2);
var = 55;
printf("i am parent pid = %d, parentID = %d, var = %d\n",getpid(),getppid(),var);
}else if(pid == 0){
var = 100;
printf("i am child pid = %d, parentID = %d, var = %d\n",getpid(),getppid(),var);
}
printf("var = %d\n",var);
return 0;
}
输出结果:
i am child pid = 6493, parentID = 6492, var = 100
var = 100
i am parent pid = 6492, parentID = 6371, var = 55
var = 55
证明全局变量父子独享
现阶段父子进程间遵循读时共享写时复制的原则 。
父子进程共享 1.文件描述符 2.mmap建立的映射区
fork之后父子进程谁先执行谁后执行 取决于内核所使用的调度算法
gdb调试
使用gdb调试的时候 gdb只能跟踪一个进程 可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认是跟踪父进程
set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程
set follow-fork-mode parent 设置跟踪父进程
注意 一定要在fork函数调用之前设置才有效
遇到循环有多个子进程的时候 可以用条件断点设置需要调试的子进程