0
点赞
收藏
分享

微信扫一扫

一文了解进程及其通信方法


进程概念

用户空间&& 内核空间
  • 内核空间:操作系统(系统调用函数)和驱动程序运行在内核空间,内核空间是进程共享的。
  • 用户空间:应用程序运行在用户空间,用户空间是各进程私有的。注意:应用程序中的系统调用是运行在内核空间的,涉及到用户空间到内核空间再到用户空间的切换。
程序&& 进程
  • 程序:静态的源代码或可执行文件;
  • 进程:动态的运行起来的程序实例;
  • 操作系统用进程控制块PCB表示创建的每一个进程,并将所有进程以链表的形式组织起来。
  • 操作系统用进程号pid唯一标识每一个进程,当所标识的进程退出后,原来的标识号便又可以被再次使用。

通常利用​​ps aux | grep process_name​​ 来查看某进程的进程号,在程序内部使用getpid()函数查看当前进程的pid。

进程状态
  1. 运行态:进程占用CPU资源正在执行自己的命令;
  2. 就绪态:万事俱备,只欠CPU;
  3. 阻塞态:等待某事件(IO输入事件、阻塞函数返回)发生。
  • ps命令下的进程状态
  • aux选项组合
  • ​ps aux | more​
  • a:显示一个终端的所有进程;
  • u:显示进程的归属用户及内存使用情况;
  • x:显示没有关联控制终端的进程。
  • 参数解释
    • USER:进程的归属用户(创建者)
    • PID:进程id
    • %CPU:进程占用CPU资源的百分比
    • %MEM:进程占用内存资源的百分比
    • VSZ:进程使用的虚拟内存大小
    • RSS:进程使用的物理内存大小
    • TTY:当前进程关联的终端
    • STAT:当前进程的状态
      • D:disinterruptible,不可被打断的睡眠状态,通常是等待某IO的结束。
      • R:running,进程正在运行或已就绪(只要被调度就随时可执行)
      • S:sleep,表示可被打断的、因被阻塞而进入的睡眠状态的进程
      • T:terminal,暂停状态(CTRL+z,位于后台暂停或处于除错状态)
      • X:死掉的状态,ps命令看不到该状态,因为一死进程就退出了。
      • Z:zombie,僵尸状态(虽已退出,但未被回收)
      • t:被跟踪状态,即进程正在被gdb调试
      • +:状态字后面跟着+号表示这是一个前台进程(占用终端),没有+表示这是一个后台进程。
  • axjf组合
    ​ps axjf | more​
  • a:显示一个终端的所有进程;
  • x:显示没有关联控制终端的进程。
  • j:显示进程归属的组gid、会话sid、父进程id
  • f:以ASCII码的形式显示出进程的层次关系。
进程调度
  • 进程是抢占式执行的。
  • 调度算法
  • 先来先服务
  • 短作业优先
  • 高优先级优先
  • 时间片轮转(毫秒级别:5—800)
  • 并发/并行
  • 并发:微观上看是交替执行,即多个进程轮流占用一个CPU资源,只是CPU时间片在人看来很短,让你以为多个进程是同时运行的。
  • 并行:微观上看是同时执行,即多个进程分别占用一个CPU资源,同时运行各自的代码。
    介绍ps aux命令查看到的信息含义:
进程信息
  • 进程上下文
  • 涵义:在进程运行时,系统中的各个寄存器中保存了进程当前运行时的信息,这些信息就是进程上下文。当进程被调度器调离时,为了下一次能接着当前状态执行,就要将当前寄存器中的值保存到栈帧中,以便下一次进程被调度时恢复进程上次的执行状态。
  • 程序计数器(pc寄存器):最重要的一个上下文信息,它记录了进程下一次执行的开始位置(某一条汇编指令的地址)。
  • 内存指针:指向程序地址空间
  • 记账信息:记录使用CPU时长、占用内存大小
  • IO信息:保存进程打开的文件信息
  • 每一个进程被创建的时候都会默认打开三个文件:
  • stdin:标准输入(scanf()、getchar())
  • stdout:标准输出( printf())
  • stderr:标准错误输出(perror())

对于每一个进程,操作系统都会以进程号pid在/proc目录下创建一个文件夹,里面存放该进程的相关信息。在​​/proc/进程号/fd​​目录下有三个软连接文件: 0(标准输入域)、1(标准输出)、2(标准错误)

  • 一文了解进程及其通信方法_进程间通信

进程退出
  • 正常退出
  • 从​​return​​语句返回
  • 调用​​exit()​​函数返回(stdlib.h)
  • 调用​​_exit()​​函数返回(unistd.h)
  • 异常退出
  • Ctrl + c
  • 指令异常(访问不存在的地址,如NULL等)
  • 运算错误(除0)
  • exit & _exit的区别
  • 一文了解进程及其通信方法_进程属性_02

  • exit函数比_exit函数多两步:
  1. 执行用户自定义的清理函数

#include<stdlib.h>
/*
* 功能:注册一个函数,在进程终止的时候调用
* 被调用的函数只能是返回值类型为void的无参函数
*/
int atexit( void(*function)(void) )

  1. 冲刷缓冲区、关闭流等。
  • 缓冲区:C标准库定义的,而非内核。建立缓冲区的目的是减少IO次数(IO操作比较耗费时间)。当触发刷新缓冲区的条件后,缓冲区的内容才会继续进行IO操作。
  • 触发刷新缓冲区的条件
  • exit()
  • main()函数中的return语句
  • fflush函数
  • 回车符\n
  • 冲刷方式
  1. 全缓冲(当缓冲区写满了一次性进行IO)
  2. 行缓冲(在输入输出中,遇到换行符时标准IO库进行IO操作)
  3. 不带缓冲(标准IO库不对字符进行缓冲)
  • 关闭流:标准输入、标准输出、标准错误
进程等待
  • 为什么要进程等待
  • 已知子进程先于父进程退出,父进程如果不管不顾,子进程就会变成僵尸进程,进而造成内存泄漏问题。
  • 进程一旦进入僵尸状态,就会刀枪不入,“杀人魔王”kill -9也无能为力,因为谁也没有办法杀死一个死去的进程。但是,父进程给子进程的任务它完成的如何,我们需要知道。
  • 父进程通过进程等待的方式,回收子进程资源,进而获取子进程退出状态信息。
  • 总而言之:父进程进行进程等待,等待子进程退出之后回收子进程的退出状态信息,防止子进程变成僵尸进程
  • 进程等待函数
  1. ​wait​
  1. 原型:

#include<sys/wait.h>
/*
* 返回值:成功返回被等待进程的pid;失败返回-1
* 参数:输出型参数,获取子进程状态,不关心可以设置为NULL
*/
pid_t wait(int* status);

  1. 特点
  • 阻塞,直到等待的子进程退出。
  1. ​waitpid​
  1. 原型:

pid_t waitpid(pid_t pid,int* status,int options);
/*
返回值: 1.成功返回收集到的进程的pid;
2.如果设置了WNOHANG选项,且没有子进程可以收集,则返回0;
3.失败返回-1 并设置errno

参数:
1、pid:
pid = -1: 等待任意一个子进程,与wait等效
pid > 0 : 等待进程ID与pid相等的子进程
2、status:输出型参数,获取子进程状态,不关心可以设置为NULL
3、options:
WNOHANG:非阻塞
*/

  1. 特点
  • 当参数options被设置为WNOHANG后,为非阻塞:
  • 当调用一个非阻塞函数的时候,函数会判断资源是否准备好。如果准备好则执行函数功能并返回;如果没准备好,则函数报错后返回(注:函数功能并没有完成)
  • 要点:非阻塞要搭配循环来使用
  1. 关于ststus参数
  1. 子进程正常退出:高字节存储子进程的退出状态,第7位的coredump标志位设为0,低字节的低7位也设为0。
  2. 子进程非正常退出:低字节存储子进程的终止信号,第7位的coredump标志位设为1。
  3. 一文了解进程及其通信方法_进程分类_03

  4. 子进程正常退出情况下,获取status的值

//wait.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
pid_t ret = fork();

if(-1 == ret)
{
return -1;
}
else if(0 == ret)
{
//子进程
printf("I am child process,pid is %d\n",getpid());
sleep(5);
exit(100);
}
else
{
//父进程
int status = 0;
pid_t result = wait(&status);
if(-1 == result)
{
return -1;
}
else if(result > 0)
{
if((status&0x7f) == 0)
{
//子进程是正常退出的
printf("child process return code is %d\n",(status>>8)&0xff);
}
else
{
//子进程异常退出
printf("child process receive signal is %d, coredump flag is %d\n ",status&0x7f,(status>>7)&0x1);
}
}
}
return 0;
}

//waitpid.c
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>

int main()
{
pid_t pid = fork();
if(-1 == pid)
{
return -1;
}
else if(0 == pid)
{
//child
printf("I am child, my pid is %d\n",getpid());
sleep(5);
exit(100);
}
else
{
//parent
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(pid,&status,WNOHANG);
}while(ret == 0);
if(ret == 0)
{
//没有已退出的进程可以回收
return 0;
}
else if(-1 == ret)
{
//调用出错
return -1;
}
else
{
//正常返回,返回收集到的子进程的pid
if((status&0x7f) == 0)
{
//子进程正常退出
printf("child process return code id %d\n",(status>>8)&0xff);
}
else
{
printf("child process recivice signal is %d,coredump flag is %d\n",(status&0x7f),(status>>7)&0x1);
}
}
}
return 0;
}

  1. 异常情况下获取status的值

//wait.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
pid_t ret = fork();

if(-1 == ret)
{
return -1;
}
else if(0 == ret)
{
//在子进程中构造异常(非法访问)退出场景
int* point = NULL;
*point = 100;
}
else
{
//父进程
int status = 0;
pid_t result = wait(&status);
if(-1 == result)
{
return -1;
}
else if(result > 0)
{
if((status&0x7f) == 0)
{
//子进程是正常退出的
printf("child process return code is %d\n",(status>>8)&0xff);
}
else
{
//子进程异常退出
printf("child process receive signal is %d, coredump flag is %d\n ",status&0x7f,(status>>7)&0x1);
}
}
}

return 0;
}

此处你测试出来的coredump标志位如果是0,那是因为你没有设置coredump文件。

可以通过​​ulimit -a​​查看core file size是否为0,若是,则用​​ulimit -c unlimited​​将其设置为无限制大小。

//waitpid.c
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>


int main()
{
pid_t pid = fork();
if(-1 == pid)
{
return -1;
}
else if(0 == pid)
{
//child
printf("I am child, my pid is %d\n",getpid());
sleep(5);

//测试异常退出
int* p = NULL;
*p = 100;
}
else
{
//parent
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(pid,&status,WNOHANG);
}while(ret == 0);
if(ret == 0)
{
//没有已退出的进程可以回收
return 0;
}
else if(-1 == ret)
{
//调用出错
return -1;
}
else
{
//正常返回,返回收集到的子进程的pid
if((status&0x7f) == 0)
{
//子进程正常退出
printf("child process return code id %d\n",(status>>8)&0xff);
}
else
{
printf("child process recivice signal is %d,coredump flag is %d\n",(status&0x7f),(status>>7)&0x1);
}
}
}
return 0;
}

进程程序替换

父子进程共享代码段,当我们要让子进程执行不同程序的时候,就需要让子进程调用进程替换函数,从而让子进程执行不一样的代码。本质上就是替换进程的代码段和数据段,以及更新堆栈。

一文了解进程及其通信方法_进程_04

  • exec函数族
    函数名带有l(list):以可变参数列表的方式传递参数,例如(execl、execlp、execle);
    函数名带有p(path):使用PATH环境变量搜索程序,所以不必写绝对路径,如(execlp、execvp);
    函数名带有e(env):需要用户自己维护环境变量,如(execle、execve)
    函数名带有v(vector):以字符指针数组的方式传递参数,例如(execv、execvp、execve);
  • ​execl​

int execl(const char* path,const char* arg ...);
/*
参数:
path:程序的路径名
arg :传递给可执行程序的命令行参数,第一个参数是可执行程序名;
如果要传递多个参数,则用逗号将其隔开,最后以NULL结尾。
返回值:
调用成功:加载新的程序,不再返回
调用失败:返回-1
*/
例如:execl("/usr/bin/ls","ls","-a","-l",NULL);

  • ​execlp​

int execlp(const char* file,const char* arg ...)
/*
参数:
file:可执行程序(可以不带路径,也可以带路径)
剩余参数与execl函数一致
*/
例如:execlp("ls","ls","-a","-l",NULL);

为什么execlp第一个参数不用带路径呢?
execlp这个函数会去搜索PATH这个环境变量,若可执行程序在PATH中则正常替换,执行替换后的程序;若不在PATH中,则报错返回。

  • ​execle​

int execle(const char* path,const char* arg,...,char* const envp[])
/*
参数:
相较于execl,增加了一个envp[],剩下的完全一致;
envp:用户传递的环境变量(用户在调用该函数的时候,需要自己组织环境变量传递给函数)
*/
例如:
extern char** environ; //系统自带的全局环境变量
int ret = execle("/home/mtgetenv","mygetenv",NULL,environ);

  • ​execv​

int execv(const char* path,char* const argv[]);
/*
参数:
argv:以指针数组的方式传递给可执行程序的命令行参数;
剩下的与execl一致
*/
例如:
char* argv[10] = {NULL};
argv[0] = "ls";
argv[1] = "-a";
argv[2] = "-l";
int ret = execv("/usr/bin/ls",argv);

  • ​execvp​

int execvp(const char* file,char* const argv[]);
/*
参数:
file:可执行程序,可以不用带有路径,也可以带
argv:以指针数组的方式传递给可执行程序的命令行参数,
返回值与execl一致
*/

  • ​execve​

int execve(const char* path,char* const argv[],char* const envp[]);
/*
参数:
path:需要带路径的可执行程序
argv:传递给可执行程序的命令行参数,以指针数组的方式传递
envp:程序员自己组织的环境变量
返回值与execl一致
*/
例如:
extern char** environ;
char* argv[10] = {NULL};
argv[0] = "ls";
argv[1] = "-a";
argv[2] = "-l";
int ret = execve("/usr/bin/ls",argv,environ);

  • 函数之间的区别
  • execve系统调用函数,其他五个函数都属于C标准库函数;
  • 一文了解进程及其通信方法_进程_05

  • 实例:

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>


int main()
{
pid_t pid = fork();
if(pid < 0)
{
return 0;
}
else if(pid == 0)
{
printf("Before:I start replace!\n");
int ret = execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("replace failed:%d",ret);
}
else
{
printf("I am father, I prepare to wait child process!\n");
wait(NULL);

}
return 0;
}

创建子进程
  • fork函数
  • 头文件:​​#include <unistd.h>​
  • 函数原型:​​pid_t fork()​
  • 返回值:成功会有两个返回值,子进程号(>0)返回给父进程,0返回给子进程;失败返回-1。
  • 特性
  • 在命令行当中启动的进程,它的父进程就是当前的bash。
  • 父子进程是代码共享、数据独有且是相互独立运行的,各自有各自的虚拟地址空间和页表,互不干扰。
  • 父子进程是抢占式运行的。谁先谁后由调度器决定。
  • 子进程是从fork语句之后开始运行的。
  • 主要应用场景
  • 守护进程:子进程执行真正的业务(进程程序替换),父进程负责守护子进程(当子进程在执行业务的时候意外“挂掉了”,父进程负责重新启动子进程,让子进程继续提供服务)。
  • fork之后的内部机制
  1. 系统分配新的内存和内核数据结构(task_struct)给子进程;
  2. 将父进程部分数据结构拷贝至子进程;
  3. 添加子进程到系统列表中,添加到双向链表当中;
  4. fork返回,开始调度器调度(操作系统开始调度)。
  • 父子进程的执行流
  • 一文了解进程及其通信方法_环境变量_06

  • 写时拷贝
  • 通常,父子进程代码共享,父子不再写入时,数据也是共享的。但当任意一方试图写入,便会以写时拷贝的方式各自复制一份副本。具体步骤如下:
  1. 子进程的PCB和页表都是拷贝父进程的。
  2. 起初,系统并没有给子进程当中的变量重新分配空间,它还是原来父进程物理地址当中的内容。
  3. 如果父子进程都没改变某变量的值,则子进程就没必要为该变量新分配一个空间,子进程可以共享父进程的数据资源。
  4. 但如果有任意一方改变了某变量值(例如下图的页表项100),那系统就需要另外分配一块物理内存给子进程。此时父子进程通过各自的页表,指向不同的物理地址。
  • ​getppid()​​可以获取当前进程的父进程的进程号。

#include<stdio.h>
#include <unistd.h>

int main(void)
{
pid_t pid = fork();
if(-1 == pid) //创建子进程失败
{
return -1;
}
else if(0 == pid)//子进程
{
printf("This is child process,with pid:%d,ppid:%d\n",getpid(),getppid());
sleep(3);
}
else //父进程
{
printf("This is father process,with pid:%d,ppid:%d\n",getpid(),getppid());
sleep(3);
}
return 0;
}

  • 父子进程的关系
  • 代码共享性:子进程复制父进程的PCB,即共享代码空间和打开的文件资源
  • 进程独立性:父子进程各有各的虚拟地址空间,确保在执行的时候数据不会相互干扰。

子进程从fork函数后的下一条指令处开始执行,此时,父子进程竞争使用CPU来运行自己的代码,而在一个进程被剥离CPU的时候,程序计数器就会记录下一条要执行的指令。因此,子进程的程序计数器起始记录的一定是fork函数执行完毕后的第一条汇编指令(其实就是将函数返回值移动到某个寄存器的汇编指令)

一般来说,父进程主要起管家的作用,主要是安排各个子进程什么时候干干什么,而子进程就相当于保姆,具体负责干实事的。

环境变量
  • 概念:用来指定操作系统运行的一些参数。也就是说,操作系统通过环境变量来找到运行时的一些资源。执行命令的时候,帮助用户找到该命令在哪一个位置。
  • 常见的环境变量
  1. PATH
  • 指定可执行程序的搜索路径。程序员执行的命令之所以能够被找到,就是环境变量的作用。
  • 验证:使用​​which + 命令​​查找该命令所在的路径。
  1. HOME
  • 登录到Linux操作系统的家目录
  1. SHELL
  • 当前的命令行解释器,默认是"/bin/bash"

查看当前环境变量,使用env命令来查看;

查看某环境变量值,使用echo $[环境变量名称]

  • 环境变量的组织方式
  • 环境变量名称 = 环境变量的值(使用:进行间隔)
  • 系统当中的环境变量是有多个时,每一个环境变量的组织方式都是key(环境变量名称)= value(环境变量的值,多个值之间用:隔开)
  • 通过字符指针数组的方式组织,数组最后的元素以NULL结尾(当程序拿到环境变量的时候,读取到NULL,说明已经读取完毕)
  • ​char* env[] ​​​:本质上是一个数组,数组的元素是​​char *​​​,每一个​​char *​​都指向一个环境变量(key = value)
  • 环境变量对应的文件
  1. 系统级文件
  2. 用户级文件
  • 修改环境变量
  1. 命令范式
    ​​​export 环境变量名称 = $环境变量名称 :新添加的环境变量内容​
  2. 修改方式
  • 命令行当中直接修改
  • 文件中修改
  • 扩展
  • 如何让自己的程序。不加 ./ 直接使用程序名称执行?两种方式:
  1. 将我们的程序放在/user/bin下面(不推荐)
  2. 设置环境变量:在PATH环境变量当中增加可执行程序的路径
    环境变量的组织方式
  • 获取环境变量
  • 通过main函数的参数获取
  • main函数参数的含义:可以在main函数内通过循环的方式打印环境变量的内容(循环条件:env[i] != NULL)
  • 验证:for循环打印的内容和命令行直接输入env的结果一致

#include<stdio.h>
#include <unistd.h>

int main(int argc,char* argv[],char* env[])
{
int i = 0;

//打印参数个数
printf("参数个数为%d\n",argc);

//打印参数
for(; argv[i] != NULL; i++)
{
printf("%s\n",argv[i]);
}

//打印环境变量
i = 0;
for(i = 0;env[i]!=NULL;i++)
{
printf("%s\n",env[i]);
}
return 0;
}

  • 使用env命令
  • 使用getenv函数:查看特定PTAH环境变量的内容

#include<stdio.h>
#include<stdlib.h>

int main()
{
char* ret = NULL;
ret = getenv("PATH");
printf("%s\n",ret);
return 0;
}

  • environ——全局环境变量
  • ​extern char** environ​​:这个是全局的外部变量,在lib.so当中定义,使用的时候需要extern关键字。

#include<stdio.h>
#include <unistd.h>
int main()
{
extern char** environ;
int i = 0;
for(; environ[i]!=NULL;i++)
{
printf("%s\n",environ[i]);
}
return 0;
}

进程的分类

  • 进程的正常退出步骤:
  1. 子进程调用exit函数退出
  2. 父进程调用wait函数对子进程进行回收
  • 僵尸进程:执行了步骤1,但还未执行步骤2。
  • 托孤进程:父进程先于子进程退出,子进程变为托孤进程,且交予linux的1号进程(init进程)回收。

僵尸进程

  • 具体形成过程
  1. 子进程退出后,自动给父进程发送SIG_CHLD信号;
  2. 父进程收到子进程的SIG_CHLD信号,但该信号的处理方式为忽略
  3. 子进程因未被父进程回收,导致在内核中的PCB未得到释放;
  4. 因此,子进程就变成了僵尸进程,通过ps命令可发现其状态被系统标记为Z。

#include <stdio.h>
#include <unistd.h>

//僵尸进程:子进程先于父进程退出
int main()
{
int ret = fork();
if(ret<0)
{
return -1;
}
else if(ret == 0)
{
//子进程代码
printf("the child process exit!\n");
}
else
{
//父进程代码
while(1)
{
printf("I am parent process!\n");
sleep(1);
}
}
}

  • 解决方案
  • 过多的僵尸进程的存在,必然会大量占用系统内存(PCB资源不能得到释放),因此就会造成内存泄漏。所以,强烈推荐由父进程进行进程等待
  • 通过命令行的​​kill​​命令进行回收
  • 普通终止:​​kill pid​​,但可能会杀不死。
  • 强行终止:​​kill -9 pid​​。

孤儿进程

  • 具体形成过程
    父进程先于子进程退出后,因为父进程没有了,所以子进程就变成孤儿了。注意:没有孤儿状态!!!
  • 模拟代码

#include <stdio.h>
#include <unistd.h>
//孤儿进程:父进程先于子进程退出

int main()
{
int ret = fork();
if(ret<0)
{
return -1;
}
else if(ret == 0)
{
//子进程代码
while(1)
{
printf("I am child process!\n");
printf("pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//父进程代码
sleep(1);
printf("I am parent process!\n");
printf("pid:%d ppid:%d\n",getpid(),getppid());
}
}

  • 解决方案
  • 虽然孤儿进程的父进程已经被杀死了,但父进程在死前将它所有的子进程都托付给了1号进程,所以孤儿进程又叫托孤进程。在孤儿进程退出的时候,系统的1号进程(init进程)便会对托付给他的孤儿进程进行回收,不会像僵尸进程那样一直占用系统内存(PCB资源不能得到释放)。
  • 什么是1号进程:1号进程(内核态)由0号进程创建,负责执行内核的部分初始化工作及进 行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程, 即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号…的若干终端注册进程getty。
  • 孤儿进程有危害吗?
    孤儿进程没有危害。因为孤儿进程在正常退出后,被一号进程领养,不会形成僵尸进程

进程的属性

进程的虚拟地址空间

  • 因为系统的物理空间资源是有限的,且不可能为每一个进程都分配4G的地址空间,所以为了提高多进程并行运行及最大限度的提高存储资源的利用率,操作系统引入了MMU(内存管理系统),它负责为每一个进程分配虚拟的4G地址空间,并在程序运行时将程序当中的虚拟地址转换为物理地址。
  • 既然是虚拟出来的地址,那当程序在访问某虚拟地址的时候,是需要MMU将其转化成物理地址的,且这些虚拟地址只有在使用的时候才会由系统映射为物理地址的。
  • 映射方式
  • 分段式:通过段表建立连接
  • 物理地址 = 段号 + 段内偏移
  • 段号 :指向某段的起始地址
  • 段页式
  • 物理地址 = 段号 + 页号 + 页内偏移
  • 段号:指向某页表地址
  • 页号:指向某一页(块)的地址
  • 页表式:虚拟地址和物理地址事先都以页为单位进行划分,并通过页表建立联系。
  • 物理地址 = 页号 + 页内偏移;
  • 页号 = 虚拟地址/页大小;(通常1页=4KB)
  • 页内偏移 = 虚拟地址%页大小

每个进程都有自己的页表(在进程控制块PCB中有页表地址),子进程最初的页表映射的内容就是来自父进程的。但后面子进程在运行的时候,可能就会有不同的映射了。

进程优先级

  • 为什么要有优先级
  • 系统进程多,CPU少,进程之间具有竞争性。为了高效完成任务,更合理竞争相关资源,于是便有了优先级
  • 概念
  • 进程获取CPU资源分配的先后顺序就是进程的优先级。
  • 系统用优先级PRI和友好值NI来表示一个进程的优先关系。
  • PRI & NI
  • 在未引入NI前,PRI值越小的进程就拥有越高的优先级;
  • 引入友好值NI后,进程的优先级 = PRI + NI
  • NI(nice)的取值范围是[-20, 19]
  • 修改进程优先级
  • 在Linux下就是通过调整进程的nice值来调整进程优先级的。
  • 命令行执行​​top​​,动态查看系统当前各进程的运行情况
  • 键入​​r​​键,再输入进程号pid,选择你要修改的进程。
  • 输入[-20, 19]之间的值,为进程设置友好度NI。

进程间通信

每一个进程通过各自的进程虚拟地址空间对存储在物理内存中的进程数据进行访问(通过各自的页表的映射关系,访问到物理内存)。从进程的角度看,每个进程都认为自己有4G(在32位操作系统平台下)的空间,至于物理内存当中是如何存储,页表如何映射,进程是不清楚的。这也造就了进程的独立性,确保进程间的数据不会窜。但当两个进程之间需要交换数据时,就无法方便的交换信息了。因此就出现了进程间通信这个课题。

通信目的

  • 数据传输
  • 资源共享
  • 事件通知
  • 进程控制

通信方式

  1. 早期Unix系统的ipc
  1. 管道
  2. 信号
  3. fifo
  1. system-v的ipc——贝尔实验室
  1. system-v 消息队列
  2. system-v 信号量
  3. system-v 共享内存套接字ipc——BSD伯克利大学
  4. 。。。
  1. p操作系统ix的ipc——IEEE
  1. p操作系统ix 消息队列
  2. p操作系统ix 信号量
  3. p操作系统ix 共享内存

无名管道

无名管道的本质就是内核当中的一块缓冲区,供进程进行读写,达到交换数据的目的。

  • 头文件​#include <unistd.h>​
  • 函数原型:​​int pipe(int pipefd[2])​
  • pipefd为输出型参数,其中中​​pipefd[0]​​是管道的读端,​​pipefd[1]​​是管道的写端
  • 成功返回0,失败返回-1
  • 从内核角度窥探管道创建动作
  • 进程调用pipe接口后,就会在内核当中产生一块缓冲区。该缓冲区有读写两端,相应的,也会产生两个文件描述符,分别与读端和写端相对应。
  • 当前的进程控制块PCB中有一个​​struct files_struct ​​​结构体指针​​files​​​,在files_struct结构体中有一个结构体指针数组​​ fd_array[ ]​​​,
    该数组中的每一个元素都是一个文件结构体指针​​​struct file*​​,该指针指向的就是一个描述文件信息的结构体,而该数组的下标就是文件的文件描述符。
  • 特点
  • 它是一个没有名字的特殊文件,无法用open函数打开,但可以用cl操作系统e关闭。
  • 只能通过子进程继承父进程的文件描述符的形式来使用;
  • 管道(缓冲区)的大小为64k。
  • 管道是基于字节流服务的,管道里的数据被读一次就自动删除了,如果没有数据继续写入,则第二次读便会因为读不到数据而被阻塞。
  • write和read操作无名管道的输入输出文件描述符时,默认是阻塞性的。当然用户可以通过​​fcntl()​​函数手动改变它们为非阻塞性的。以设置非阻塞读为例,其步骤如下:
  • 获取fd[0]本身属性:​​read_ret = fcntl(fd[0], F_GETFL);​
  • 给fd[0]加上非阻塞属性:​​fcntl(fd[0], F_SETFL, read_ret | O_NONBLOCK);​
  • 数据传输是单向的(只能从管道的写端流向管道的读端),即半双工。
  • 在使用read函数读取管道数据的时候,可以自定义每次读取的字节数,且当读取的字节数小于4096字节的时候,能确保本次读取操作是原子性的。
  • 所有文件描述符被关闭(进程退出)之后,无名管道被销毁。
  • 使用步骤
  1. 父进程调用pipe函数创建无名管道;
  2. 父进程调用fork函数创建子进程;
  3. 分别在父子进程中利用cl操作系统e函数关闭没用到的端口(读或写)
  4. 调用write/read函数读写数据
  5. 利用cl操作系统e函数关闭读写端口
  • 代码实例:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <error.h>
#include <string.h>

#define MAX_DATA_LEN 256

int main()
{
pid_t pid;
int pipe_fd[2];
int status;
char buf[MAX_DATA_LEN];
const char data[] = "Pipe test program\n";
int real_read,real_write;

memset((void *)buf,0,sizeof(buf));
/*创建管道*/
if(pipe(pipe_fd) < 0){
printf("pipe create failed\n");
exit(1);
}

/*创建子进程*/
if((pid = fork()) == 0){
/*子进程关闭写描述符*/
cl操作系统e(pipe_fd[1]);
/*子进程读管道内容*/
if((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0){
printf("%d bytes read from the pipe is: %s\n",real_read,buf);
}
/*关闭子进程读描述符*/
cl操作系统e(pipe_fd[0]);
exit(0);
}else if(pid > 0){
/*父进程关闭读描述符*/
cl操作系统e(pipe_fd[0]);
if((real_write = write(pipe_fd[1], data, strlen(data)) != -1)){
printf("%d bytes write to the pipe which is: %s\n",real_write,data);
}
/*关闭父进程写描述符*/
cl操作系统e(pipe_fd[1]);
/*回收子进程*/
wait(&status);
exit(0);
}
}

一文了解进程及其通信方法_进程间通信_07

有名管道

有名管道可以支持非父子进程间的通信

  • 头文件​#include <unistd.h>​
  • 函数原型:​​int mkfifo(const char* pathname, mode_t moed)​
  • pathname:要创建的有名管道的路径名。
  • mode:有名管道的读写权限,用八进制数表示(如0664)
  • 成功返回0,失败返回-1
  • 特性
  • 可通过命令行创建有名管道:​​mkfifo fifo_name​
  • 代码实例

system-V 信号量

  • 作用:保护共享资源(同步/互斥),本质就是一个计数器。
  • 互斥是指每个进程需要先获取到信号量才可以访问临界资源,如果获取失败,就会阻塞等待。
  • 同步是指某进程在某个点试图获取信号量(阻塞操作),即等待另一个进程完成某项工作到达某一个同步点的时候释放得信号量。
  • 用法
  1. 定义一个唯一的key(ftok)
  2. 构造一个信号量(semget)
  3. 初始化信号量(semctl + SETVA)
  4. 对信号量进行PV操作(semop)
  5. 删除信号量(semctl + RMID)
  • 函数介绍
  1. ​semget()​
  • 功能:获取信号量对象的ID
  • 函数原型​int semget(key_t key, int nsems, int semflg);​
  • key:信号量键值
  • nsems:信号量数量
  • shmflg:
  • IPC_CREAT:信号量不存在则创建
  • mode:新创建的信号量权限
  • 返回值:成功则返回信号量ID,失败返回-1
  1. ​semclt()​
  • 功能:获取/设置/删除信号量的相关属性
  • 函数原型:​​int semctl(int semid, int semnum, int cmd, union semun arg);​
  • semid:信号量ID
  • semnum:信号量编号
  • cmd:
  • IPC_STAT:获取信号量的属性信息
  • IPC_SET:设置信号量的属性
  • IPC_RMID:删除信号量
  • IPC_SETVAL:设置信号量的值
  • arg:
    union semun
    {
    int val;
    struct semid_ds *buf;
    }
  • 返回值:成功由cmd类型决定,失败返回-1
  1. ​semop()​
  • 功能:信号量的PV操作函数
  • 函数原型:​​int semctl(int semid, struct sembuf *sops, size_t nsops);​
  • semid:信号量ID
  • sops:信号量操作结构体
    struct sembuf
    {
    short sem_num; //信号量编号
    short sem_op; //信号量PV操作
    ​ short sem_flg; //信号量行为(SEM_UNDO表示进程推出后由系统回收信号量)
    }
  • nops:信号量数量
  • 返回值:成功则返回0,失败返回-1
  • 函数封装

为了在具体编程时更加简易的使用信号量,一般我们会编写一个信号量使用的API

//sem.c
#include <sys/ipc.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>

union semun
{
int val;
struct semid_ds *buf;
}

/*初始化信号量*/
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_unino.val = init_value;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1){
printf("initialize semaphor failed\n");
exit(-1);
}
return 0;
}

/*删除信号量*/
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1){
perror("delete semaphor");
exit(-1);
}
return 0;
}

/*P操作*/
int sem_p(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0; //单个信号量的编号为0,即从0开始编
sops.sem_op = -1; //P操作用减1
sops.sem_flg = SEM_UNDO; //表示系统自动释放进程退出后未回收的信号量

if(semop(sem_id, &sops, 1) == -1){
perror("P operation");
exit(-1);
}
return 0;
}

/*V操作*/
int sem_v(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = 1; //V操作用加1
sops.sem_flg = SEM_UNDO;

if(semop(sem_id, &sops, 1) == -1){
perror("V operation");
exit(-1);
}
return 0;
}

测试代码:

//test.c
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define DELAY_TIME 3

int main(void)
{
pid_t pid;
int sem_id;

sem_id = semget((key_t)6666, 1, 0666|IPC_CREAT); //创建一个信号量
init_sem(sem_id);
pid = fork();
if(pid == -1){
perror("fork");
}else if(pid == 0){
printf("Child process will wait for some seconds...\n");
sleep(DELAY_TIME);
printf("the child process is running...\n");
sem_v(sem_id);
}else{
sem_p(sem_id); //等待子进程执行完
printf("the parent process is running\n");
sem_v(sem_id);

del_sem(sem_id);
}
return 0;
}

system-V 共享内存

  • 作用:高效率进程间传输大量数据,不同的进程通过各自的页表将某同一段物理内存空间映射到自己进程的虚拟空间中,然后不同的进程就可以通过操作自己虚拟空间的地址来达到读写数据(通信)的目的。
  • 一文了解进程及其通信方法_环境变量_08

  • 用法
  1. 定义一个唯一的key(ftok)
  2. 构造一个共享内存对象(shmget)
  3. 共享内存映射(shmat)
  4. 解除共享内存映射(shmdt)
  5. 删除共享内存(shmctl)
  • 头文件:​​#include <sys/shm.h>​
  • 函数介绍
  1. ​shmget()​
  • 功能获取共享内存对象的ID(操作句柄)
  • 函数原型
  • ​int shmget(key_t key, int size, int shmflg);​
  • key:共享对象键值(标识符)
  • size:共享内存大小
  • shmflg:属性信息
  • IPC_CREAT:若共享内存不存在则新建,否则返回该共享内存的ID。
  • IPC_CREAT | IPC_EXCL:若共享内存存在则报错,若不存在就新建,意思就是说返回的一定是新建的共享内存ID。
  • mode:新创建的共享内存权限,与前面的属性相或。
  • 返回值:成功则返回共享内存ID,失败返回-1
  1. ​shmat()​
  • 功能将共享内存映射到进程的虚拟空间
  • 函数原型
  • ​void* shmat(int shmid, const void *shmaddr, int shmflg);​
  • shmid:共享内存ID
  • shmaddr:共享内存的映射地址,NULL为自动分配
  • shmflg:以什么权限将共享内存附加到进程当中
  • SHM_RDONLY:只读方式映射
  • 0:可读写方式映射
  • 返回值:成功则返回映射到的虚拟地址,失败返回NULL
  1. ​shmdt()​
  • 功能解除共享内存映射
  • 函数原型
  • ​int shmdt(const void *shmaddr);​
  • shmaddr:共享内存的映射地址(虚拟地址)
  • 返回值:成功则返回0,失败返回-1
  1. ​shmctl()​
  • 功能获取/设置/删除共享内存的相关属性
  • 函数原型
  • ​int shmctl(int shmid, int cmd, struct shmid_ds *buf);​
  • shmid:共享内存ID
  • cmd:告诉函数它需要完成什么任务
  • IPC_STAT:获取共享内存的属性信息
  • IPC_SET:设置共享内存的属性
  • IPC_RMID:删除共享内存,此时buf参数填NULL
  • buf:属性缓冲区,是一个输出型参数(用户提供地址空间,函数负责填写内容),其结构体定义为:

struct shmid_ds {
struct ipc_perm shm_perm; //共享内存权限
size_t shm_segsz; //缓冲区字节数
time_t shm_atime; //最后修改时间
time_t shm_dtime;
time_t shm_ctime;
pid_t shm_cpid; //所有者的进程号
pid_t shm_lpid; //最后映射到的进程号
shmatt_t shm_nattch; //连接数
...
}

  • 一文了解进程及其通信方法_进程分类_09

  • 返回值:成功返回值由cmd类型决定,失败返回-1
  • 特性
  • 覆盖写:向共享内存写数据时,会覆盖旧数据(清空共享内存区)
  • 反复读:从共享内存读数据时,不会影响旧数据(区别于字节流的管道)
  • 可通过命令行删除现有共享内存:
  • 执行​​ipcs​​​查看系统当前进程间通信情况(消息队列、共享内存、信号量)
  • 一文了解进程及其通信方法_进程间通信_10

  • 执行​​ipcrm -m shmid​​​删除id为shmid的共享内存
  • 一文了解进程及其通信方法_进程分类_11

  • 删除共享内存的时候,若共享内存附加的进程数量不为0,则会将该共享内存的key变成0x00000000表示当前共享内存不能被其他进程所附加,共享内存的状态会被设置为destory。附加的进程一旦全部退出之后,该共享内存在内核的结构体会被操作系统释放。
  • 一文了解进程及其通信方法_进程属性_12

  • 代码示例
  1. 两个进程(以父子进程为例,但也可以是非父子进程)分别通过​​shmget​​函数创建/获取共享内存,且这两个进程在创建/获取共享内存时,其参数设置(大小)必须一致,否则获取到就不是同一个共享内存。
  2. 分别调用​​shmat​​接口将共享内存附加到自己的进程虚拟地址空间去。
  3. 进程间通信。
  4. 通信结束后将进程和共享内存分离。

//test.c
#include <sys/types.h>
#include "sem.h"

#define DELAY_TIME 3

int main(void)
{
pid_t pid;
int sem_id;
int shm_id;
char *addr;

sem_id = semget((key_t)6666, 1, 0666|IPC_CREAT); //创建一个信号量
shm_id = shmget((key_t)7777, 1024, 0666|IPC_CREAT); //创建共享内存对象

/*初始化信号量*/
init_sem(sem_id,0);
/*调用fork()函数*/
pid = fork();
if(pid == -1){
perror("fork failed\n");
}else if(pid == 0){
printf("Child process will wait for some seconds...\n");
sleep(DELAY_TIME);
/*映射可读写的共享内存*/
addr = shmat(shm_id, NULL, 0);
if(addr == (void *)-1){
printf("shmat in child error\n");
exit(-1);
}
/*向共享内存中写入内容*/
memcpy(addr, "hello world!",strlen("hello world!")+1);
printf("the child process is running...\n");
sem_v(sem_id);
}else{
sem_p(sem_id); //等待子进程执行完
printf("the parent process is running\n");
/*映射共享内存地址*/
addr = shmat(shm_id, NULL, 0);
if(addr == (void *)-1){
printf("shmat in parent error\n");
exit(-1);
}
printf("shared memory string:%s\n",addr);
/*解除共享内存映射*/
shmdt(addr);
/*删除共享内存映射*/
shmctl(shm_id, IPC_RMID, NULL);
}
return 0
}

消息队列

  • 原理:由内核维护消息的链表,系统中存在多个​​msgqueue​​,并用消息队列ID(qid)唯一区分。在进行进程间通信的时候,一个进程将消息追加到MQ的尾端,另一个进程从消息队列里取走数据,但不一定严格按照先进先出的原则取数据,也可以按照消息类型字段来取。
  • 接口函数
  • ​int msgget(key_t key, int msgflg);​
  • key:消息队列的标识符
  • msgflg:创建标志
  • IPC_CREAT:若不存在则新建;
  • mode:按位或上权限(八进制数,如0664)
  • 返回值:成功则返回消息队列的qid,失败返回-1,并设置errno
  • ​int masgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);​
  • msgid:消息队列的ID(qid)
  • msgp:指向待发送消息结构体(struct msgbuf)的指针;

struct msgbuf{
long mtype; //消息类型,必须大于0
char mtext[1]; //消息数据,但程序员可根据实际需要修改此数组大小
};

  • msgsz:要发送消息的长度(就是struct msgbuf结构体中char mtext[]数组的大小)
  • msgflg:创建标记
  • 0:阻塞发送
  • IPC_NOWAIT:非阻塞发送
  • 返回值:成功返回0,失败返回-1,并设置errno
  • ​int msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);​
  • msgid:消息队列的ID(qid)
  • msgp:指向待接收消息结构体(struct msgbuf)的指针;
  • msgsz:要接收消息的长度(就是由struct msgbuf结构体中char mtext[]数组的长度)
  • msgtyp:
  • 等于0:读取队列中第一条消息
  • 大于0:读取队列中类型为msgtyp的第一条消息,但如果指定了MSG_EXCEPT,则读取类型为非msgtyp的第一条消息。
  • 小于0:读取队列中最小类型小于或等于msgtyp绝对值的第一条消息
  • msgflg:创建标记
  • 0:阻塞发送
  • IPC_NOWAIT:非阻塞接收
  • 返回值:成功返回实际读取消息的字节数,失败返回-1,并设置errno
  • ​int msgctl(int msgid, int cmd, struct msgid_ds *buf);​
  • msgid:消息队列的ID(qid)
  • cmd:控制命令
  • IPC_RMID:删除消息队列
  • IPC_STAT:获取消息队列的当前状态。
  • buf:存储消息队列的相关信息
  • 0:阻塞发送
  • 返回值:成功返回实际读取消息的字节数,失败返回-1,并设置errno
  • 用法
  1. 用​​msgget​​函数新建/获取一个消息队列;
  2. 定义一个消息缓冲区​​msgbuf​​,用来发送或接收信息
  3. 对消息队列进行发送(​​msgsnd​​​)或接收(​​msgrcv​​)
  4. 删除消息队列(什么周期跟随系统)。
  • 代码实例:一个进程发送,一个进程接收。注意:不同进程间使用消息队列进行通信的时候,需要获取相同的消息队列标识符。

//msg_send.c
#include <string.h>
#include <stdio.h>
#include <sys/msg.h>
#include <fcntl.h>

struct msgbuf
{
long mtype;
char mtext[255];
};

int main(void)
{
int ret;
int qid = msgget(0x12341234, IPC_CREAT|0664); //kyy为12341234,由用户自己定义
if(qid < 0){
perror("msgget:");
exit(1);
}
printf("queue ID is %d\n", qid);

/*创建并初始化消息的buffer*/
struct msgbuf msgbuffer;
msgbuffer.mtype = 2; //指定消息类型为2
const char * str_send = "I will send to ...";
strcpy(msgbuffer.mtext, str_send);

//发送消息
ret = msgsnd(qid, &msgbuffer, sizeof(msgbuffer.mtext), 0);
if(ret < 0){
perror("msgsnd:");
exit(1);
}
return 0;
}

//msg_receive.c
#include <string.h>
#include <stdio.h>
#include <sys/msg.h>
#include <fcntl.h>

struct msgbuf
{
long mtype;
char mtext[255];
};

int main(void)
{
int ret;
int qid = msgget(0x12341234, IPC_CREAT|0664);
if(qid < 0){
perror("msgget:");
exit(1);
}
printf("queue ID is %d\n", qid);

/*创建接收消息的buffer*/
struct msgbuf msgbuffer;

/*接收消息*/
ret = sgrcv(qid, &msgbuffer, sizeof(msgbuffer.mtext), 0, 0);
if(ret < 0){
perror("msgrcv:");
exit(1);
}

/*打印*/
printf("Received strings:%s\n",msgbuffer.mtext);
return 0;
}


举报

相关推荐

0 条评论