多进程编程
多进程的基础
什么叫做进程
一个动态运行的内存
动态体现在:动态的分配内存空间,上下文切换
为什么要学习多进程编程
了解单核处理器同时运行多个软件的原理
进程的内存管理
每个程序运行的进程,都会占据4个gb的内存空间
1-3个gb为每一个进程独立拥有的内存
3-4这一个gb的内存为所有进程共享的内存空间
注意:我们说到的内存空间,其实都是虚拟内存
3个gb的虚拟内存是由1个gb的物理内存映射出来的,物理内存会借助磁盘的空间来映射虚拟内存
进程的组成部分
进程控制段
用来存放进程的进程编号,进程状态,起始地址等各种各样的问题
数据段
用来存放进程运行过程当中,产生的各种数据
代码段
用来存放该进程的运行代码,结构体自定义信息
进程的分类
交互进程
在终端上运行的进程,通过标准输出流和标准输入流与用户进行实时交互的进程
批量处理进程
所有在后台运行的进程,都是批量处理进程,很明显后台运行的进程就无法用终端控制,故这些进程无法与用户进行交互,那么就可以找一个专门的时间节点,专门处理这些进程
后台进程:./a.out (可执行文件)& 后台软件:软件名&
结束进程/软件:kile -9 进程pid号
守护进程
是一种能在后台持续运行的,拥有高权限,与用户完全脱离的,带有服务性质的进程
例如:usb、网络进程服务器
如何创建守护进程
成为一个孤儿进程
脱离原先的用户会话组
pid_t setsid(void);
功能描述:成为一个新的会话组的组长,并返回该会话组的id
切换工作目录到根目录
chdir
设置掩码为0000或0111,为之后重定向创建文件做准备
umask
重定向标准输入流,标准输出流,标准错误流到根目录的自定义文件中,彻底脱离于用户的关系
进程编号pid
每个进程都有自己的独立pid编号
用户能够使用的进程编号范围为3~32767(2^15-1)
32767为早期发行的版本使用short类型的变量进行存储
现在的pid编号使用int类型的变量来存储此时的最大值为65535
pid的分配规则
从已有的最大pid编号往后循环寻找未使用
若往后的编号都已使用完毕则会从头开始寻找未使用的编号,此时再寻找不到编号则会打开失败
3个特殊的进程
0号进程
idel进程:0号进程会反复循环运行来尝试启动一号进程,当一号进程运行时,0号进程就会停止运行,反之如果没有进程运行就会启动0号进程
1号进程
分为两个状态
内核态1号进程
负责初始化所有的硬件设备,当所有的硬件初始化完毕后,内核态的一号进程就会转变为用户态的一号进程
用户态1号进程
是操作系统中,所有进程的始祖进程,也就是说系统中所有的进程都是直接或间接的由一号进程创建并打开的
2号进程
2号kthread进程
负责进程与进程之间的调度工作,即为进程的的先后运行顺序及能否运行都由其调度
进程相关的shell指令
ps -ef查看当前系统中所有进程的关系
UID PID PPID C STIME TTY TIME CMD root 1 0 0 08:50 ? 00:00:02 /sbin/init auto noprompt root 2 0 0 08:50 ? 00:00:00 [kthreadd] root 3 2 0 08:50 ? 00:00:00 [rcu_gp] root 4 2 0 08:50 ? 00:00:00 [rcu_par_gp] root 5 2 0 08:50 ? 00:00:00 [slub_flushwq] root 6 2 0 08:50 ? 00:00:00 [netns] root 8 2 0 08:50 ? 00:00:00 [kworker/0:0H-events_highpri] root 10 2 0 08:50 ? 00:00:00 [mm_percpu_wq]
UID
进程的拥有者(创建者)
PID
进程的编号
PPID
该进程的父进程的编号(父进程指创建子进程的进程)
C
被链接数
STIME
开始运行时间
tty
依赖的终端,?代表不依赖任何终端
cmd
进程名
ps -ajx:用来显示进程的状态,以及运行的时间
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 0 1 1 1 ? -1 Ss 0 0:02 /sbin/init auto noprompt 0 2 0 0 ? -1 S 0 0:00 [kthreadd] 2 3 0 0 ? -1 I< 0 0:00 [rcu_gp] 2 4 0 0 ? -1 I< 0 0:00 [rcu_par_gp] 2 5 0 0 ? -1 I< 0 0:00 [slub_flushwq] 2 6 0 0 ? -1 I< 0 0:00 [netns]
STAT:进程的状态
进程的状态可以通过man ps来查看
在手册中搜索PROCESS STATE CODES
pa -aux:查看所有进程的cpu和内存占有率
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.2 167992 11792 ? Ss 08:50 0:02 /sbin/init auto noprompt root 2 0.0 0.0 0 0 ? S 08:50 0:00 [kthreadd] root 3 0.0 0.0 0 0 ? I< 08:50 0:00 [rcu_gp] root 4 0.0 0.0 0 0 ? I< 08:50 0:00 [rcu_par_gp] root 5 0.0 0.0 0 0 ? I< 08:50 0:00 [slub_flushwq] root 6 0.0 0.0 0 0 ? I< 08:50 0:00 [netns]
kill指令
kill -9 pid
功能:杀死该进程
杀死进程的操作不是由kill决定,而是9决定
实际含义为向进程号为pid的进程发送9指令
数字指令可通过shell指令kill -l查看
pdof 进程名
获取该进程的pid
top
就是window中的任务管理器
以动态的形式显示当前系统中的所有进程
pstree
以树状图的形式显示系统中的所有任务
进程的状态切换(importent)
起始态
运行态
一个进程被2号进程分配了运行资源(时间片) 从而进入运行状态
受到阻塞进入静止态,正常运行结束进入就绪态
就绪态
一个能准备好随时运行的进程(所有运行条件全部满足,仅仅因为没有获得时间片而没有进入运行状态)
只有就绪状态可被分配时间片或抢夺时间片,一旦获得时间片就会进入运行态
静止态
因为各种原因,导致进程不满足运行状态的条件
运行条件不满足通常都是运行受到阻塞,比如遇见sleep函数或者scanf,read等函数
满足运行条件后,解除阻塞状态,就会立即进入就绪状态(其无法直接进入就绪状态)
终止态
运行结束
进程的运行状态切换实验
通过Ctrl + z可让一个进程暂停运行
后台暂停后可通过fg + 工作号,使其重新回到前台运行
通过jobs -l查看
必须在运行进程的终端输入并查看,其他终端并不能看到当前终端的工作号
并发和并行
并发
多任务在同一个处理器上同时运行
并行
多任务在不同处理器上同时运行
fork函数
功能
在一个进程中打开另一个进程
格式
pid_t fork(void);
功能描述
一旦调用fork函数,就产生一个新的进程,作为当前进程(父进程)的子进程
注意
fork一旦调用,就会产生子进程,即fork函数之后的函数代码都会由父子进程同时进行
fork创建出来的子进程,子进程的内存会完全拷贝父进程的内存空间(除了一些进程特有的数据,例如pid,ppid这些数据)
其实子进程并不是在fork函数结束后才进行的,而是在fork函数内部,fork返回之前就已经产生,故一个fork函数会有两个返回值,父进程返回一个,子进程返回一个
返回值(两个)
fork 的父进程部分会返回子进程的pid,一个大于0的值
fork的子进程成功创建会返回0
子进程创建失败fork会返回-1
fork产生子进程之后,父子进程之间是并发运行的(同时运行),而不是父进程运行完后子进程运行
运行代码对父子进程状态进行分析
上述代码的运行效果为:AB_AB_BA_BA_AB_AB_BA......
为什么有时候是AB, 有时候是BA?
因为A输出之后,会因为sleep进入静止态,从而释放时间片,B就会立刻抢到时间片并输出自己
虽然A会先进入静止,也会先从静止态退出
但是静止态无法直接切换到运行态,所以大概率A只是比B先进入了就绪态,AB和同时处于就绪态的概率非常大,所以大家既然都在就绪态,谁先运行,谁后运行就不好说了
孤儿进程
概要
一个进程的父进程先死亡,其自身仍在运行状态下,该进程被称为孤儿进程 (在标准linux系统中,孤儿进程会被1号进程接管)
接管的目的:所有进程死亡后,其内存资源需要被其父进程接收(故孤儿进程是一个高危进程)
其为一个后台进程
getpid
格式
pid_t getpid(void)
功能描述
获取当前进程的进程号,以返回值的形式获取
getppid
格式
pid_t getppid(void)
功能描述
获取当前进程的父进程的进程号,以返回值的形式获取
僵尸进程
概要
子进程死亡,父进程存活,但父进程并未回收子进程的内存资源
其为一个高危操作:僵尸进程一旦过多,内存占有会变多,程序会变卡,pid也会因此变少,因此我们需要尽量避免出现僵尸进程
解决僵尸进程
kill -9只能将进程结束
而僵尸进程是资源未回收的问题
使僵尸进程变成一个孤儿进程,杀死其父进程,从新分配一个父进程
进程资源回收函数
wait
格式
pid_t wait(int *wstatus);
功能描述
一旦调用wait函数,就会阻塞,直到有子进程的资源被成功回收为止
参数
wstatus
这是一个int *指针, 说明其会在函数内部发生改变
改变成子进程退出时的状态
正常退出
WIFEXITED(wstatus) == 1
子进程正常退出的途径
exit
exit_t
或者在main函数里return
一般来说不需要知道进程退出时的状态
故一般直接写成wait(0)
waitpid
格式
pid_t waitpid(pid_t pid,int *wstatus, int options)
概要
这是一个高阶定制版的wait函数
可定制成非阻塞型回收,允许定制成回收指定的子进程资源或者指定的某一组子进程的资源
参数1
pid
<-1
此时传入的是一个进程会话组id的相反数。回收所有的进程会话组id == |pid|的所有的子进程
=-1
回收所有的子进程
=0
回收所有和父进程在同一个会话组中的子进程
>0
此时传入的是一个子进程的pid,表示回收某个特定的子进程
参数2
options
一般有以下两种选项
0:表示阻塞型回收
WNOHANG
表示非阻塞型回收
进程替换exec函数
格式
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
参数1
path/file
准备运行的那个程序的进程的可执行文件名的路径
参数2
list
后面的参数为arg+...
“cp”, “1.c”, "2.c", NULL
argv
后面的参数为argv
argv[4] = {“cp”, “1.c”, "2.c"} (预留一个位置函数会放NULL)
函数名中带p和不带p
file的函数,函数名中带p
当我们写的替换的进程的路径名中,没有指定路径的时候,则会去shell指令所在的路径中寻找可执行文件
如果没找到则替换失败
path的函数,函数名中不带p
当我们写的替换的进程的路径名的时候,如果不指定路径,则会去当前工作目录中查询该进程
如果没找到则进程替换失败
函数名中带e和不带e
带e的exec函数,允许通过最后一个参数来设置替换进来的进程的环境变量
参数 char * const envp[]
作业:使用多进程 + wait + exec + strtok 实现一个伪装的终端
#include <myhead.h> //返回buf数组,将stdin的文件名称输入到buf字符组中 char* getLine(char* buf,int size) { fgets(buf,size,stdin); int len = strlen(buf); //将文件的回车符号改为字符结束符'\0' if(buf[len-1]=='\n'){ buf[len-1] = 0; } } int main(int argc, const char *argv[]) { //创建一个子进程并杀死父进程使其变为孤儿进程 while(1){ int retval = fork(); if(retval > 0){ wait(0); }else{ //将孤儿进程当前的位置及所属用户输出 char* username = getlogin(); char hostname[64] = {0}; char pwd[128] = {0}; gethostname(hostname,64); getcwd(pwd,128); //输出的什么玩意??? printf("\033[1;32;10m%s@%s\033[0m:\033[1;34;10m%s\033[0m$ ",username,hostname,pwd); fflush(stdout); char* arg[20] = {0}; // find /usr/include -name stdio.h // cd .. char cmd[128] = {0}; getLine(cmd,128);//用cmd接收buf字符数组,即stdin的文件名称 //分割文件的名称??? char* retval = NULL; int i = 0; do{ if(retval == NULL){ retval = strtok(cmd," "); }else{ retval = strtok(NULL," "); } // printf("retval = %s\n",retval); arg[i++] = retval; }while(retval != NULL); //和cd比较文件名???,为什么就比较一个???? if(strcmp(arg[0],"cd")==0){ chdir(arg[1]); }else{ execvp(arg[0],arg); printf("没有找到该指令\n"); } //return 0; } } return 0; }