嵌入式养成计划-26-IO进线程----进程

一个小demo

Linux下利用文件IO函数完成多进程复制图片,父进程复制前一半,子进程复制后一半

六十一、进程

61.1 进程概述

61.1.1 何谓进程?

  • 进程是程序的一次执行过程。
    • 程序是静态的,它是存储在外存上的可执行二进制文件
    • 进程是动态的,它是程序的一次执行过程,包括了进程的创建,调度,消亡,是存在内存中的。
  • 进程是独立的,可以被调度的任务
    • Linux操作系统的进程调度方式:时间片轮询机制
    • 当进程被创建后,会分配2ms~100ms不等的时间,等待cpu调度。当cpu调度到该进程,不论任务是否执行完毕,只要时间片结束,cpu资源会切换到下一个进程。
  • 进程在被调度的时候,系统会给进程分配和释放各种资源。(例如:cpu资源,内存资源,进程调度块(PCB))

61.1.2 进程的五态模型(重点)

  • 创建态 : 刚创建出来
  • 就绪态 : 资源都获取到了,等待分配时间片
  • 运行态 : 获得时间片,正在运行
  • 阻塞态 : 因为某些资源没有获取到,停下来等待资源的获取
  • 终止态 : 进程结束(一瞬间,说没就没)
    嵌入式养成计划-26-IO进线程----进程_第1张图片

61.1.3 进程的七态模型

本小节内容参考了这篇文章: 进程的状态转换

  • 七态模型在五态模型的基础上增加了挂起就绪态(ready suspend)和挂起等待态(blocked suspend)。
    • 挂起就绪态:进程具备运行条件,但目前在外存中,只有它被对换到内存才能被调度执行。
    • 挂起等待态:表明进程正在等待某一个事件发生且在外存中。
  • 当系统资源尤其是内存资源已经不能满足进程运行的要求时,必须把某些进程挂起(suspend),对换到磁盘对换区中,释放它占有的某些资源,暂时不参与低级调度。起到平滑系统操作负荷的目的。

嵌入式养成计划-26-IO进线程----进程_第2张图片

引起进程挂起的原因是多样的,主要有:

  1. 终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。
  2. 父进程的请求。有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各子进程间的活动。
  3. 负荷调节的需要。当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
  4. 操作系统的需要。操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
  5. 对换的需要。为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。

61.1.4 进程的内存管理

  • 每个进程都会分配4G的内存空间。(虚拟内存空间)
  • 0~3G是用户空间代码使用,进程之间的用户空间相互独立
  • 3G~4G是内核空间代码使用,内核空间是所有进程共享的
  1. 虚拟内存空间与物理内存空间的关系
  • 映射关系。用户只能访问用户空间的虚拟地址
  • cache和主存之间也是映射关系
    • 有三种映射方式:直接映射,组相连映射,全相联映射
    • 其中组相连映射又可以分为多种的组相连,如二路组相连映射,四路组相连映射等
  • 物理内存空间: 硬件上(内存条上)真正存在的存储空间。
  • 虚拟地址空间: 程序运行后,会有4G的虚拟地址空间,需要使用的时候由虚拟地址映射到物理地址上使用。
    • 32位OS : 4G,2^32个字节,计算一下就知道是4GB了(B—>Byte)
    • 64位OS : 256TB,目前只用到了48位,2^48字节,计算一下就知道是256TB了(B—>Byte)
  • 使用虚拟内存的目的:cpu能够动态分配内存,能够让每个进程都认为自己是独享内存空间
    嵌入式养成计划-26-IO进线程----进程_第3张图片

61.1.5 进程的内存分布

嵌入式养成计划-26-IO进线程----进程_第4张图片
嵌入式养成计划-26-IO进线程----进程_第5张图片

61.1.6 进程是资源分配的最小单位

  1. 以进程为单位申请释放内存空间(用户空间:静态存储区,堆区,栈区)
  2. 以进程为单位分配CPU资源,及时间片
  3. 以进程为单位分配文件描述符:1024个。
  4. 以进程为单位管理自己的虚拟地址空间,在使用的时候会映射到物理地址空间上.
  5. … …

61.1.7 进程标识

  • 操作系统会给每一个进程分配一个id号,这个id号被称之为pid号(进程号,process id)。PID号是进程的唯一标识。
  1. 主要的进程标识
    1. PID: process id 进程号
    2. PPID: parent process id 父进程号
    3. PGID: process group id 进程组号。
      进程组:若干个进程的集合称之为进程组,默认情况下,新创建的子进程会继承父进程的组id;
    4. SID: session is 会话组号;
  2. 特殊的进程标识
    1. PID: 0 idle进程 操作系统引导程序,创建1号,2号进程。
    2. PID: 1 init进程 初始化内核的各个模块,当内核启动完成后,用于收养孤儿进程(没有父进程的进程)。
    3. 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 
	#include 
	
	pid_t fork(void);
参数:
    
返回值:
	成功, >0 在父进程中返回新建的子进程的pid号
	      =0 在子进程中返回0;
	失败,在父进程中返回-1,更新errno.且此时没有子进程被创建。
  1. 当父进程执行fork后,会创建一个子进程。子进程用户空间中的所有资源都是从父进程拷贝过来的。
    子进程不会运行当前创建它的那个fork函数,以及fork函数以上的内容。
    原因如下:
    在CPU内部,每执行一条微指令时都会事先获取下一条微指令的地址,即在刚准备执行fork函数的功能时就会获取fork功能的下一条微指令的地址,所以子进程会从fork功能的下一条微指令开始执行
    (注意:微指令不是一条代码,是比汇编更低一级的机器指令的分步执行步骤,是一条代码被细分到CPU能够理解的那一层级的操作步骤)
  2. 拷贝完毕的一瞬间,父子进程的用户空间分布完全一致。即子进程用户空间中的初始资源是从父进程拷贝过来的。
    但由于父子进程用户空间相互独立,所以父子进程根据CPU的调度运行各自的代码,申请各自空间内的变量,互不干扰。
  3. 父子进程映射的物理地址,根据写时拷贝原理。
    1. 写时拷贝:我的解释是,在修改时,需要写回主存(内存)时才会对变量进行拷贝供子进程使用
    2. 当父子进程均不修改物理地址空间中的内容是,此时父子进程映射的物理地址是一致的。
    3. 若其中一个进程要修改物理地址空间中的内容是,需要申请一块新的物理地址空间给子进程映射。
  4. 父子进程文件描述符表的关系:
    文件描述符表在用户空间中,所以父进程会拷贝一份文件描述符表给子进程。 嵌入式养成计划-26-IO进线程----进程_第6张图片

61.3.2 getpid/getppid

功能:
	获取当前进程的pid号/获取父进程的pid号
原型:
	#include 
	#include 
	
	pid_t getpid(void);
	pid_t getppid(void);
返回值:
	永远成功,返回pid号(当前进程的id号)、ppid号(当前进程的父进程的id号,parent id)

61.3.3 _exit / exit

  • _exit
功能:
	当运行到该函数的时候会退出进程,且**不会刷新缓冲区****直接销毁缓冲区**。
原型:
	#include 
	
	void _exit(int status);
参数:
	int status:传递子进程的退出状态值给父进程。父进程可以通过wait/waitpid函数接收。
	可以传入任意整型数
  • exit
功能:
	当运行到该函数的时候会退出进程,但是 **会刷新缓冲区**
原型:
	#include 
	
	void exit(int status);
参数:
	int status:传递子进程的退出状态值给父进程。父进程可以通过wait/waitpid函数接收。
	可以传入任意整型;

注意: 若只退出子进程,而其父进程没有给子进程收尸的时候,子进程的资源没有被回收,此时子进程会变成僵尸进程

61.3.4 wait / waitpid

  • wait
功能:
	阻塞函数,阻塞等待任意一个子进程退出,解除阻塞;
	接收子进程的退出状态值。(exit _exit main函数调用return传递出来的值);
	回收任意一个子进程的资源(收尸,防止尸变--占用资源);
原型:
	#include 
	#include 
	
	pid_t wait(int *wstatus);
参数:
	int *wstatus:接收子进程传递回来的退出状态值,若不想接收,则填NULL;
返回值:
	>0, 成功返回退出的子进程的PID号;
	=-1,函数运行失败,更新errno; 
  • 从wait(&wstatus) 中的wstatus中提取,exit(status)函数传递回来的status的值。
    1. wstatus这32bit中,只有[15, 8]bits用于存储status;
    2. 若要同wstatus中提取status的值,需要先右移8bit
    3. 为了防止高位有数据干扰,需要提取出右移后的低8bit
    4. (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 
	#include 
	pid_t waitpid(pid_t pid, int *wstatus, int options);
参数:
	pid_t pid:
	//< -1   阻塞等待指定进程组下的任意一个子进程退出。进程组id = -pid参数;
	
	-1     阻塞等待当前进程下的任意一个子进程退出。此时功能等价于wait函数
	
	//0      阻塞等待当前进程组下的任意一个子进程退出。
	
	> 0    阻塞等待子进程,子进程的id号 == pid参数;
       
	int *wstatus:接收子进程传递回来的退出状态值,若不想接收,则填NULLint options:
		0:阻塞方式,若指定的子进程没有退出,则该函数阻塞。直到指定的子进程退出后,解除阻塞.
		WNOHANG:非阻塞方式,若指定的子进程没有退出,则该函数也不阻塞,且没有回收到资源。 
		
返回值:
	成功,>0, 成功回收到的子进程的pid号;
		=0, 函数运行成功,但是指定的子进程未退出,此时没有收到子进程的资源。    
	失败,(例如进程下没有子进程的时候)返回-1,更新errno; 

通过一下实验得到结果,注意:

1. 若没有子进程,则函数运行失败。
2. 函数只能回收子进程的资源,无法跨辈回收,
	例如爷收孙,子收父,兄弟进程相互回收,均无法成立。

61.4 进程相关的函数

61.4.1 孤儿进程

  1. 没有父进程的进程,称为孤儿进程。即父进程退出,子进程不退出,此时子进程就是孤儿进程。
  2. 孤儿进程会被init进程(1号进程)收养。
  3. 孤儿进程会脱离终端控制。无法被前端的ctrl+c杀死,但是可以被kill
  4. 孤儿进程是活着的进程,没有危害,因为在运行功能

代码示例 :

#include 
#include 
#include 

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 僵尸进程

  1. 子进程退出,父进程不退出,且父进程没有回收退出的子进程的资源。此时子进程会变成僵尸进程。
  2. 僵尸进程是有危害的,必须回收,因为在占用资源而没有执行功能
    1. 占用进程号
    2. 占用内存空间,占用物理空间,占用进程调度块,占用cpu资源等等…
  3. 僵尸进程只能被回收,无法被再次杀死,因为已经死了
  4. 如何回收僵尸进程:
    1. 父进程退出后,在其下方的僵尸进程会被内核自动回收。
    2. 用过wait和waitpid函数回收。
      缺点:阻塞方式会影响父进程正常运行,非阻塞方式有可能回收不到。
    3. 结合信号的方式回收僵尸进程:当子进程退出后,通知父进程收尸。

代码示例:

#include 
#include 
#include 

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 守护进程

  • 又称之为幽灵进程。
  1. 守护进程脱离于终端,且运行在后端。
  2. 守护进程在执行过程中不会将信息显示在任何终端上,避免影响前端任务执行。且不会被任何终端产生的终端信息所打断。
  3. 守护进程目的:需要周期性执行某个任务或者周期性等待处理某些事情的时候,为了避免影响前端执行或者被前端信息打断的时候,可以使用守护进程。
  • 守护进程的创建步骤:
  1. 创建孤儿进程:所有工作都在子进程中执行,从形式上脱离终端控制。
    fork()函数
    方便结束父线程后子线程还存在
  2. 创建新的会话组:使子进程完全独立出来,防止兄弟进程对其有影响
    setsid() 函数
  3. 修改当前孤儿进程的运行目录为不可卸载的文件系统:例如根目录,/tmp
    防止运行目录被删除后,导致进程崩溃
    chdir() 函数
  4. 重设文件权限掩码:
    umask(0), 一般清零;
  5. 关闭所有文件描述符
    从父进程继承过来的文件描述符不会用到,浪费资源。
  • setsid() 函数
功能:
	创建一个新的进程组和会话组,成为该进程组和会话组组长;
原型:
	#include 
	#include 
	
	pid_t setsid(void);
  • chdir() 函数
功能:修改运行目录;
	#include 
	
	int chdir(const char *path);
如:
chdir("/");

代码示例:

#include 

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;
}

你可能感兴趣的:(网络,服务器,linux,c++)