一个小demo
Linux下利用文件IO函数完成多进程复制图片,父进程复制前一半,子进程复制后一半
六十一、进程
61.1 进程概述
61.1.1 何谓进程?
- 进程是程序的一次执行过程。
- 程序是静态的,它是存储在外存上的可执行二进制文件
- 进程是动态的,它是程序的一次执行过程,包括了进程的创建,调度,消亡,是存在内存中的。
- 进程是独立的,可以被调度的任务
- Linux操作系统的进程调度方式:时间片轮询机制
- 当进程被创建后,会分配2ms~100ms不等的时间,等待cpu调度。当cpu调度到该进程,不论任务是否执行完毕,只要时间片结束,cpu资源会切换到下一个进程。
- 进程在被调度的时候,系统会给进程分配和释放各种资源。(例如:cpu资源,内存资源,进程调度块(PCB))
61.1.2 进程的五态模型(重点)
- 创建态 : 刚创建出来
- 就绪态 : 资源都获取到了,等待分配时间片
- 运行态 : 获得时间片,正在运行
- 阻塞态 : 因为某些资源没有获取到,停下来等待资源的获取
- 终止态 : 进程结束(一瞬间,说没就没)
61.1.3 进程的七态模型
本小节内容参考了这篇文章: 进程的状态转换
- 七态模型在五态模型的基础上增加了挂起就绪态(ready suspend)和挂起等待态(blocked suspend)。
- 挂起就绪态:进程具备运行条件,但目前在外存中,只有它被对换到内存才能被调度执行。
- 挂起等待态:表明进程正在等待某一个事件发生且在外存中。
- 当系统资源尤其是内存资源已经不能满足进程运行的要求时,必须把某些进程挂起(suspend),对换到磁盘对换区中,释放它占有的某些资源,暂时不参与低级调度。起到平滑系统操作负荷的目的。
引起进程挂起的原因是多样的,主要有:
- 终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。
- 父进程的请求。有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各子进程间的活动。
- 负荷调节的需要。当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
- 操作系统的需要。操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
- 对换的需要。为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。
61.1.4 进程的内存管理
- 每个进程都会分配4G的内存空间。(虚拟内存空间)
- 0~3G是用户空间代码使用,进程之间的用户空间相互独立。
- 3G~4G是内核空间代码使用,内核空间是所有进程共享的。
- 虚拟内存空间与物理内存空间的关系
- 映射关系。用户只能访问用户空间的虚拟地址
- cache和主存之间也是映射关系
- 有三种映射方式:直接映射,组相连映射,全相联映射
- 其中组相连映射又可以分为多种的组相连,如二路组相连映射,四路组相连映射等
- 物理内存空间: 硬件上(内存条上)真正存在的存储空间。
- 虚拟地址空间: 程序运行后,会有4G的虚拟地址空间,需要使用的时候由虚拟地址映射到物理地址上使用。
- 32位OS : 4G,2^32个字节,计算一下就知道是4GB了(B—>Byte)
- 64位OS : 256TB,目前只用到了48位,2^48字节,计算一下就知道是256TB了(B—>Byte)
- 使用虚拟内存的目的:cpu能够动态分配内存,能够让每个进程都认为自己是独享内存空间
61.1.5 进程的内存分布
61.1.6 进程是资源分配的最小单位
- 以进程为单位申请释放内存空间(用户空间:静态存储区,堆区,栈区)
- 以进程为单位分配CPU资源,及时间片
- 以进程为单位分配文件描述符:1024个。
- 以进程为单位管理自己的虚拟地址空间,在使用的时候会映射到物理地址空间上.
- … …
61.1.7 进程标识
- 操作系统会给每一个进程分配一个id号,这个id号被称之为pid号(进程号,process id)。PID号是进程的唯一标识。
- 主要的进程标识
- PID: process id 进程号
- PPID: parent process id 父进程号
- PGID: process group id 进程组号。
进程组:若干个进程的集合称之为进程组,默认情况下,新创建的子进程会继承父进程的组id; - SID: session is 会话组号;
- 特殊的进程标识
- PID: 0 idle进程 操作系统引导程序,创建1号,2号进程。
- PID: 1 init进程 初始化内核的各个模块,当内核启动完成后,用于收养孤儿进程(没有父进程的进程)。
- PID: 2 kthreadd进程 用于进程间调度。
61.2 查看进程相关的shell指令
61.2.1 ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
显示进程占计算机的资源百分比。
61.2.2 ps -ajx
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
功能:显示当前进程的家族关系。
例如:让a.out进入死循环防止程序结束后可以输入下面的指令查看
ps -ajx|grep a.out
61.2.3 pidof
功能:根据进程名字获取PID号;
pidof a.out 查看所有进程名为a.out的,进程的id号
7412 7390
61.2.4 top
功能:实时显示进程状态
输入q退出
top -d 刷新秒数
61.2.5 pstree
功能:显示进程关系树
61.2.6 kill
kill -9 pid 根据pid号杀死进程
killall -9 进程名字 根据进程名字杀死进程
61.2.7 进程状态
D 无法被中断的阻塞状态
R 运行状态
S 可以被终端的阻塞状态(sleep)
T 被挂起的状态
t 被追踪的状态(gdb调试工具)
X 死亡状态,死亡是一瞬间的事情,永远不会被捕获到
Z 僵尸进程,当进程退出后,父进程没有给它收尸。
For BSD formats and when the stat keyword is used, additional characters may be displayed:
< 高优先级
N 低优先级
L 有些页被锁进内存
s 会话组组长,代表有子进程的进程
l 多线程
+ 运行在前端
61.3 进程相关的函数
61.3.1 fork
功能:创建一个子进程;
原型:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
参数:
返回值:
成功, >0 在父进程中返回新建的子进程的pid号
=0 在子进程中返回0;
失败,在父进程中返回-1,更新errno.且此时没有子进程被创建。
- 当父进程执行fork后,会创建一个子进程。子进程用户空间中的所有资源都是从父进程拷贝过来的。
子进程不会运行当前创建它的那个fork函数,以及fork函数以上的内容。
原因如下:
在CPU内部,每执行一条微指令时都会事先获取下一条微指令的地址,即在刚准备执行fork函数的功能时就会获取fork功能的下一条微指令的地址,所以子进程会从fork功能的下一条微指令开始执行
(注意:微指令不是一条代码,是比汇编更低一级的机器指令的分步执行步骤,是一条代码被细分到CPU能够理解的那一层级的操作步骤) - 拷贝完毕的一瞬间,父子进程的用户空间分布完全一致。即子进程用户空间中的初始资源是从父进程拷贝过来的。
但由于父子进程用户空间相互独立,所以父子进程根据CPU的调度运行各自的代码,申请各自空间内的变量,互不干扰。 - 父子进程映射的物理地址,根据写时拷贝原理。
- 写时拷贝:我的解释是,在修改时,需要写回主存(内存)时才会对变量进行拷贝供子进程使用
- 当父子进程均不修改物理地址空间中的内容是,此时父子进程映射的物理地址是一致的。
- 若其中一个进程要修改物理地址空间中的内容是,需要申请一块新的物理地址空间给子进程映射。
- 父子进程文件描述符表的关系:
文件描述符表在用户空间中,所以父进程会拷贝一份文件描述符表给子进程。
61.3.2 getpid/getppid
功能:
获取当前进程的pid号/获取父进程的pid号
原型:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
返回值:
永远成功,返回pid号(当前进程的id号)、ppid号(当前进程的父进程的id号,parent id)
61.3.3 _exit / exit
- _exit
功能:
当运行到该函数的时候会退出进程,且**不会刷新缓冲区**,**直接销毁缓冲区**。
原型:
#include <unistd.h>
void _exit(int status);
参数:
int status:传递子进程的退出状态值给父进程。父进程可以通过wait/waitpid函数接收。
可以传入任意整型数
- exit
功能:
当运行到该函数的时候会退出进程,但是 **会刷新缓冲区**
原型:
#include <stdlib.h>
void exit(int status);
参数:
int status:传递子进程的退出状态值给父进程。父进程可以通过wait/waitpid函数接收。
可以传入任意整型;
注意: 若只退出子进程,而其父进程没有给子进程收尸的时候,子进程的资源没有被回收,此时子进程会变成僵尸进程
61.3.4 wait / waitpid
- wait
功能:
阻塞函数,阻塞等待任意一个子进程退出,解除阻塞;
接收子进程的退出状态值。(exit _exit main函数调用return传递出来的值);
回收任意一个子进程的资源(收尸,防止尸变--占用资源);
原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
参数:
int *wstatus:接收子进程传递回来的退出状态值,若不想接收,则填NULL;
返回值:
>0, 成功返回退出的子进程的PID号;
=-1,函数运行失败,更新errno;
- 从wait(&wstatus) 中的wstatus中提取,exit(status)函数传递回来的status的值。
- wstatus这32bit中,只有[15, 8]bits用于存储status;
- 若要同wstatus中提取status的值,需要先右移8bit
- 为了防止高位有数据干扰,需要提取出右移后的低8bit
- (wstatus>>8) & 0xff 或者 (wstatus & 0xff00) >> 8
- 使用宏----WEXITSTATUS(wstatus):提取退出状态值的
#define __WEXITSTATUS(status) (((status) & 0xff00) >> 8)
使用方法:
WEXITSTATUS(wstatus)
-
WIFEXITED(wstatus):判断子进程是否正常退出
- 若正常退出返回真,否则返回假
- 正常退出:调用exit _exit 主函数调用return退出,均为正常退出。
-
waitpid
功能:
阻塞函数,阻塞等待指定的一个子进程退出,解除阻塞;
接收该子进程的退出状态值。(exit _exit main函数调用return传递出来的值);
回收该一个子进程的资源(收尸);
原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数:
pid_t pid:
//< -1 阻塞等待指定进程组下的任意一个子进程退出。进程组id = -pid参数;
-1 阻塞等待当前进程下的任意一个子进程退出。此时功能等价于wait函数
//0 阻塞等待当前进程组下的任意一个子进程退出。
> 0 阻塞等待子进程,子进程的id号 == pid参数;
int *wstatus:接收子进程传递回来的退出状态值,若不想接收,则填NULL;
int options:
0:阻塞方式,若指定的子进程没有退出,则该函数阻塞。直到指定的子进程退出后,解除阻塞.
WNOHANG:非阻塞方式,若指定的子进程没有退出,则该函数也不阻塞,且没有回收到资源。
返回值:
成功,>0, 成功回收到的子进程的pid号;
=0, 函数运行成功,但是指定的子进程未退出,此时没有收到子进程的资源。
失败,(例如进程下没有子进程的时候)返回-1,更新errno;
通过一下实验得到结果,注意:
1. 若没有子进程,则函数运行失败。
2. 函数只能回收子进程的资源,无法跨辈回收,
例如爷收孙,子收父,兄弟进程相互回收,均无法成立。
61.4 进程相关的函数
61.4.1 孤儿进程
- 没有父进程的进程,称为孤儿进程。即父进程退出,子进程不退出,此时子进程就是孤儿进程。
- 孤儿进程会被init进程(1号进程)收养。
- 孤儿进程会脱离终端控制。无法被前端的ctrl+c杀死,但是可以被kill
- 孤儿进程是活着的进程,没有危害,因为在运行功能
代码示例 :
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//创建子进程,退出父进程,子进程不退出
pid_t cpid = fork();
if(0 == cpid)
{
while(1)
{
printf("this is child %d %d\n", getppid(), getpid());
sleep(1);
}
}
return 0;
}
61.4.2 僵尸进程
- 子进程退出,父进程不退出,且父进程没有回收退出的子进程的资源。此时子进程会变成僵尸进程。
- 僵尸进程是有危害的,必须回收,因为在占用资源而没有执行功能
- 占用进程号
- 占用内存空间,占用物理空间,占用进程调度块,占用cpu资源等等…
- 僵尸进程只能被回收,无法被再次杀死,因为已经死了
- 如何回收僵尸进程:
- 父进程退出后,在其下方的僵尸进程会被内核自动回收。
- 用过wait和waitpid函数回收。
缺点:阻塞方式会影响父进程正常运行,非阻塞方式有可能回收不到。 - 结合信号的方式回收僵尸进程:当子进程退出后,通知父进程收尸。
代码示例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//子进程退出,父进程不退出,且父进程没有回收退出的子进程的资源。
pid_t cpid = fork();
if(cpid > 0)
{
while(1)
{
printf("this is parent %d %d\n", getpid(), cpid);
sleep(1);
}
}
return 0;
}
61.4.3 守护进程
- 又称之为幽灵进程。
- 守护进程脱离于终端,且运行在后端。
- 守护进程在执行过程中不会将信息显示在任何终端上,避免影响前端任务执行。且不会被任何终端产生的终端信息所打断。
- 守护进程目的:需要周期性执行某个任务或者周期性等待处理某些事情的时候,为了避免影响前端执行或者被前端信息打断的时候,可以使用守护进程。
- 守护进程的创建步骤:
- 创建孤儿进程:所有工作都在子进程中执行,从形式上脱离终端控制。
fork()函数
方便结束父线程后子线程还存在 - 创建新的会话组:使子进程完全独立出来,防止兄弟进程对其有影响
setsid() 函数 - 修改当前孤儿进程的运行目录为不可卸载的文件系统:例如根目录,/tmp
防止运行目录被删除后,导致进程崩溃
chdir() 函数 - 重设文件权限掩码:
umask(0), 一般清零; - 关闭所有文件描述符
从父进程继承过来的文件描述符不会用到,浪费资源。
- setsid() 函数
功能:
创建一个新的进程组和会话组,成为该进程组和会话组组长;
原型:
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
- chdir() 函数
功能:修改运行目录;
#include <unistd.h>
int chdir(const char *path);
如:
chdir("/");
代码示例:
#include <unistd.h>
int main(int argc, const char *argv[])
{
//创建孤儿进程:所有的任务放在子进程中运行,形式上脱离终端控制
pid_t cpid = fork();
if(0 == cpid)
{
//创建新的会话:使子进程完全独立出来,脱离其他亲缘关系进程的控制
pid_t pid = setsid();
// printf("pid=%d\n", pid);
//修改运行目录为不可卸载的文件系统:例如根目录,一般约定俗称,若有工作日志要输出,则运行在/tmp目录下。
chdir("/");
//重设文件权限掩码:;一般清0(umask(0))
umask(0);
//关闭所有文件描述符:子进程的文件描述符继承父进程的,包括了0 1 2.
for(int i=0; i<getdtablesize(); i++)
close(i);
while(1)
{
//周期性执行的功能代码
sleep(1);
}
}
return 0;
}