Linux中的进程概念及其通信方法

进程概念

用户空间&& 内核空间
  • 内核空间:操作系统(系统调用函数)和驱动程序运行在内核空间,内核空间是进程共享的。

  • 用户空间:应用程序运行在用户空间,用户空间是各进程私有的。注意:应用程序中的系统调用是运行在内核空间的,涉及到用户空间到内核空间再到用户空间的切换。

    image-20220418225304832
程序&& 进程
  • 程序:静态的源代码或可执行文件;

  • 进程:动态的运行起来的程序实例;

    • 操作系统用进程控制块PCB表示创建的每一个进程,并将所有进程以链表的形式组织起来。
    • 操作系统用进程号pid唯一标识每一个进程,当所标识的进程退出后,原来的标识号便又可以被再次使用。

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

进程状态
  1. 运行态:进程占用CPU资源正在执行自己的命令;
  2. 就绪态:万事俱备,只欠CPU;
  3. 阻塞态:等待某事件(IO输入事件、阻塞函数返回)发生。
    image-20220414223116653
  • ps命令下的进程状态

    • aux选项组合

      • ps aux | more

        • a:显示一个终端的所有进程;
        • u:显示进程的归属用户及内存使用情况;
        • x:显示没有关联控制终端的进程。

        image-20220407224822062

        • 参数解释
          • 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(标准错误)

    • image-20220414232041643

进程退出
  • 正常退出

    • return语句返回
    • 调用exit()函数返回(stdlib.h)
    • 调用_exit()函数返回(unistd.h)
  • 异常退出

    • Ctrl + c
    • 指令异常(访问不存在的地址,如NULL等)
    • 运算错误(除0)
  • exit & _exit的区别

    image-20220418233055278

    • exit函数比_exit函数多两步:

      1. 执行用户自定义的清理函数

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

        • 缓冲区: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);
        
      2. 特点

        • 阻塞,直到等待的子进程退出。
    2. 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:非阻塞
        */
        
      2. 特点

        • 当参数options被设置为WNOHANG后,为非阻塞:
        • 当调用一个非阻塞函数的时候,函数会判断资源是否准备好。如果准备好则执行函数功能并返回;如果没准备好,则函数报错后返回(注:函数功能并没有完成)
        • 要点:非阻塞要搭配循环来使用
    3. 关于ststus参数

      1. 子进程正常退出:高字节存储子进程的退出状态,第7位的coredump标志位设为0,低字节的低7位也设为0。

      2. 子进程非正常退出:低字节存储子进程的终止信号,第7位的coredump标志位设为1。

        image-20220419220956060

      3. 子进程正常退出情况下,获取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;
        }
        
        image-20220419221659375
        //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;
        }
        
      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)
            {
                //在子进程中构造异常(非法访问)退出场景
                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;
        }
        

        image-20220419222301677

        此处你测试出来的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;
        }
        
        image-20220419223916692

进程程序替换

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

image-20220419230346169

  • 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标准库函数;

      image-20220419233238608

  • 实例:

    #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返回,开始调度器调度(操作系统开始调度)。
    • 父子进程的执行流

      image-20220418230747269

    • 写时拷贝

      • 通常,父子进程代码共享,父子不再写入时,数据也是共享的。但当任意一方试图写入,便会以写时拷贝的方式各自复制一份副本。具体步骤如下:

        1. 子进程的PCB和页表都是拷贝父进程的。
        2. 起初,系统并没有给子进程当中的变量重新分配空间,它还是原来父进程物理地址当中的内容。
        3. 如果父子进程都没改变某变量的值,则子进程就没必要为该变量新分配一个空间,子进程可以共享父进程的数据资源。
        4. 但如果有任意一方改变了某变量值(例如下图的页表项100),那系统就需要另外分配一块物理内存给子进程。此时父子进程通过各自的页表,指向不同的物理地址。

        image-20220418231507781

    • 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 + 命令查找该命令所在的路径。
    2. HOME

      • 登录到Linux操作系统的家目录
    3. 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。

- 输入[-20, 19]之间的值,为进程设置友好度NI。

进程间通信

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

通信目的:

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

通信方式

  1. 早期Unix系统的ipc

    1. 管道
    2. 信号
    3. fifo
  2. system-v的ipc——贝尔实验室

    1. system-v 消息队列

    2. system-v 信号量

    3. system-v 共享内存套接字ipc——BSD伯克利大学

    4. 。。。

  3. p操作系统ix的ipc——IEEE

    1. p操作系统ix 消息队列
    2. p操作系统ix 信号量
    3. p操作系统ix 共享内存
  • 现在在 Linux 中使用较多的进程间通信方式主要有以下几种:
    1. 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的 通信,有名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
    2. 信号(Signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂 的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个 中断请求效果上可以说是一样的。
    3. 消息队列(Messge Queue):消息队列是消息的链接表,包括 Posix 消息 队列 SystemV 消息队列。它克服了前两种通信方式中信息量有限的缺点,具有写 权限的进程可以按照一定的规则向消息队列中添加新消息;对消息队列有读权限的 进程则可以从消息队列中读取消息。
    4. 共享内存(Shared memory):可以说这是最有用的进程间通信方式。它使得 多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中 数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。
    5. 信号量(Semaphore):主要作为进程之间以及同一进程的不同线程之间的同 步和互斥手段。
    6. 套接字(Socket):这是一种更为一般的进程间通信机制,它可用于网络中不 同机器之间的进程间通信,应用非常广泛。

无名管道

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

  • 头文件#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*,该指针指向的就是一个描述文件信息的结构体,而该数组的下标就是文件的文件描述符。

    image-20220417155847227

  • 特点

    • 管道可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read() 和 write()等函数。但是它不是普通的文件,并不属于其他任何文件系统,并 且只存在于内核的内存空间中。
    • 它只能用于具有亲缘关系的进程之间的通信,即只能通过兄弟进程或子进程继承父进程的文件描述符的形式来使用;
    • 管道(缓冲区)的大小为64k。
    • 管道是基于字节流服务的,管道里的数据被读一次就自动删除了,如果没有数据继续写入,则第二次读便会因为读不到数据而被阻塞。
    • 管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符 fds[0]和 fds[1],其中fds[0]固定用于读管道,而 fd[1]固定用于写管道
    • 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字节的时候,能确保本次读取操作是原子性的。
    • 管道是一个没有名字的特殊文件,无法用open函数打开,但可以用close关闭(可用 close()逐个关闭各个文件描述符)。当一个管道共享多对文件描述符时,若将其中的一对读写文件描述符都删除, 则该管道就失效。

用 pipe()函数创建的管道两端处于一个进程中,由于管道是主要用于在不同进程 间通信的,因此这在实际应用中没有太大意义。实际上,通常先是创建一个管道,再 通过 fork()函数创建一子进程,该子进程会继承父进程所创建的管道,这时,父子进程 管道的文件描述符对应关系如左图所示。此时的关系看似非常复杂,实际上却已经给不同进程之间的读写创造了很好的条 件。父子进程分别拥有自己的读写通道,为了实现父子进程之间的读写,只需把无关 的读端或写端的文件描述符关闭即可。例如在右图中将父进程的写端 fd[1]和子进程 的读端 fd[0]关闭。此时,父子进程之间就建立起了一条“子进程写入父进程读取”的 通道。

image-20230208211152678

同样,也可以关闭父进程的 fd[0]和子进程的 fd[1],这样就可以建立一条“父进 程写入子进程读取”的通道。另外,父进程还可以创建多个子进程,各个子进程都继 承了相应的 fd[0]和 fd[1],这时,只需要关闭相应端口就可以建立其各子进程之间的通道。

  • 使用步骤

    1. 父进程调用pipe函数创建无名管道;
    2. 父进程调用fork函数创建子进程;
    3. 分别在父子进程中利用cl操作系统e函数关闭没用到的端口(读或写)
    4. 调用write/read函数读写数据
    5. 利用close函数关闭读写端口。
  • 代码实例

在本例中,首先创建管道,之后父进程使用 fork()函数创建子进程,之后通过关闭父进程的读描述符和子进程的写描述符,建立起它们之间的管道通信。

#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));
    
    /* 1.创建管道; */
    if(pipe(pipe_fd) < 0){
        printf("pipe create failed\n");
        exit(1);
    }
    
    /* 2.创建子进程;*/
    if((pid = fork()) == 0){
        /*子进程关闭写描述符*/
        close(pipe_fd[1]);
        /* 并通过使子进程暂停1s等待父进程已关闭相应的读描述符 */
        sleep(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);
        }
        /*关闭子进程读描述符*/
        close(pipe_fd[0]);
        exit(0);
    }else if(pid > 0){
        /*父进程关闭读描述符*/
        close(pipe_fd[0]);
        /* 父进程暂停1s等待子进程已关闭相应的写描述符 */
        sleep(1);
        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);
        }
        /*关闭父进程写描述符*/
        close(pipe_fd[1]);
        /*回收子进程*/
        waitpid(pid,&status,0);	//等效于wait(&status);
        exit(0);        
    }    
}

image-20220417160534078


注意:

  • 只有在管道的读端存在时,向管道写入数据才有意义。否则,向管道写入数 据的进程将收到内核传来的 SIGPIPE 信号(通常为 Broken pipe 错误)。
  • 向管道写入数据时,Linux 将不保证写入的原子性,管道缓冲区一有空闲区 域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数 据,那么写操作将会一直阻塞。
  • 父子进程在运行时,它们的先后次序并不能保证,因此,在这里为了保证父 子进程已经关闭了相应的文件描述符,可在两个进程中调用 sleep()函数,当然这种调用不是很好的解决方法,可通过进程之间的同步与互斥机制进行完善。

标准流管道

与 Linux 的文件操作中有基于文件流的标准 I/O 操作一样,管道的操作也支持基于文件流的模式。这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道,这里的“另一个进程”也就是一个可以进行一定操作的可执行文件,例如,用户 执行ls -l或者自己编写的程序./pipe等。由于这一类操作很常用,因此标准流 管道就将一系列的创建过程合并到一个函数 **popen()**中完成。它所完成的工作有以下几步:

  • 创建一个管道。
  • fork()一个子进程。
  • 在父子进程中关闭不需要的文件描述符。
  • 执行 exec 函数族调用。
  • 执行函数中所指定的命令。

这个函数的使用可以大大减少代码的编写量,但同时也有一些不利之处,例如, 它不如前面管道创建的函数那样灵活多样,并且用 popen()创建的管道必须使用标准 I/O 函数进行操作,但不能使用前面的 read()、write()一类不带缓冲的 I/O 函数。

与之相对应,关闭用 popen()创建的流管道必须使用函数 **pclose()**来关闭该管道流。 该函数关闭标准 I/O 流,并等待命令执行结束。

  • popen()

    • 原型:FILE *popen(const char *command, const char *type);
    • 参数:
      • command:指向的是一个以 null 结束符结尾的字符串,这个字符串包含一个 shell 命令,并被送到/bin/sh 以-c 参数执行,即由 shell 来执行。
      • type:
        • “r”:文件指针连接到 command 的标准输出,即该命令的结果产生输出;
        • “w”:文件指针连接到 command 的标准输入,即该命令的结果产生输入。
    • 返回值:
      • 成功:文件流指针
      • 失败:-1
  • pclose()

    • 原型:int pclose(FILE *stream);
    • 参数:stream为要关闭的文件流。
    • 返回值:
      • 成功:返回由 popen()所执行的进程的退出码
      • 失败:-1
  • 代码示例:在该实例中,使用 popen()来执行“ps -ef”命令。可以看出,popen()函数的使用能够使程序变得短小精悍。

    #include <stdio.h> 
    #include <unistd.h> 
    #include <stdlib.h> 
    #include <fcntl.h> 
    #define BUFSIZE 1024 
    int main() 
    { 
        FILE *fp; 
        char *cmd = "ps -ef"; 
        char buf[BUFSIZE]; 
    
        /*调用 popen()函数执行相应的命令*/ 
        if ((fp = popen(cmd, "r")) == NULL) 
        { 
            printf("Popen error\n"); 
            exit(1); 
        } 
    
        while ((fgets(buf, BUFSIZE, fp)) != NULL) 
        { 
            printf("%s",buf); 
        } 
        pclose(fp); 
        exit(0); 
    } 
    

有名管道

前面介绍的管道是无名管道,它只能用于具有亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制,它可以使互不相关的两个进程实现彼此通信。该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。 不过值得注意的是,FIFO 是严格地遵循先进先出规则的,对管道及 FIFO 的读总是从 开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如 lseek()等文件定位操作。

有名管道的创建可以使用函数 mkfifo(),该函数类似文件中的 open()操作,可以指定管道的路径和打开的模式。用户还可以在命令行使用mknod 管道名 p或者mkfifo fifo_name来创建有名管道。

  • 头文件#include <unistd.h>
  • 函数原型int mkfifo(const char* pathname, mode_t mode);
    • 参数:
      • pathname:要创建的有名管道的路径名。
      • mode:有名管道的读写权限,用八进制数表示(如0664)
    • 返回值:
      • 成功:0
      • 失败:-1

在创建管道成功之后,就可以使用 open()、read()和 write()这些函数了。与普通文件的开发设置一样,对于为读而打开的管道可在 open()中设置 O_RDONLY,对于为写而打开的管道可在 open()中设置 O_WRONLY,在这里与普通文件不同的是阻塞问题。由于普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能, 这里的非阻塞标志可以在 open()函数中设定为 O_NONBLOCK。下面分别对阻塞打开 和非阻塞打开的读写进行讨论。

(1)对于读进程

  • 若该管道是阻塞打开,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞到有数据写入。
  • 若该管道是非阻塞打开,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。即如果 FIFO 内没有数据,则读函数将立刻返回 0。

(2)对于写进程

  • 若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入。
  • 若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败。

示例代码:实例包含了两个程序,一个用于读管道,另一个用于写管道。在写管道的程序中, 用户输入的要写入的内容作为main()函数的参数。在读管道的程序里创建管道,读管道的程序会读出用户写入到管道的内容,这两个程序采用的是阻塞式读写管道模式。

  • 写管道程序:

    /* fifo_write.c */ 
    #include <sys/types.h> 
    #include <sys/stat.h> 
    #include <errno.h> 
    #include <fcntl.h> 
    #include <stdio.h> 
    #include <stdlib.h> 
    #include <limits.h> 
    
    #define MYFIFO "/tmp/myfifo" 		/* 有名管道文件名*/ 
    #define MAX_BUFFER_SIZE PIPE_BUF 	/*定义在于 limits.h 中*/ 
    
    int main(int argc, char * argv[]) /*参数为即将写入的字符串*/ 
    { 
        int fd;
        char buff[MAX_BUFFER_SIZE]; 
        int nwrite; 
    
        if(argc <= 1) 
        { 
            printf("Usage: ./fifo_write string\n"); 
            exit(1); 
        } 
        sscanf(argv[1], "%s", buff); 
    
        /* 以只写阻塞方式打开 FIFO 管道 */ 
        fd = open(MYFIFO, O_WRONLY); 
        if (fd == -1) 
        { 
            printf("Open fifo file error\n"); 
            exit(1); 
        } 
    
        /*向管道中写入字符串*/ 
        if ((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0) 
        { 
            printf("Write '%s' to FIFO\n", buff); 
        } 
        close(fd); 
        exit(0); 
    } 
    
  • 读管道程序

    /*fifo_read.c */ 
    #include <sys/types.h> 
    #include <sys/stat.h> 
    #include <errno.h> 
    #include <fcntl.h> 
    #include <stdio.h> 
    #include <stdlib.h> 
    #include <limits.h> 
    
    #define MYFIFO "/tmp/myfifo" 		/* 有名管道文件名*/ 
    #define MAX_BUFFER_SIZE PIPE_BUF 	/*定义在于 limits.h 中*/ 
    
    int main() 
    { 
        char buff[MAX_BUFFER_SIZE]; 
        int fd; 
        int nread; 
        /* 判断有名管道是否已存在,若尚未创建,则以相应的权限创建*/ 
        if (access(MYFIFO, F_OK) == -1) 
        { 
            if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST)) 
            { 
                perror("mkfifo"); 
                exit(1); 
            } 
        } 
        /* 以只读阻塞方式打开有名管道 */ 
        fd = open(MYFIFO, O_RDONLY); 
        if (fd == -1) 
        { 
            printf("Open fifo file error\n"); 
            exit(1); 
        } 
    
        while (1) 
        { 
            memset(buff, 0, MAX_BUFFER_SIZE); 
            if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) 
            { 
                printf("Read '%s' from FIFO\n", buff); 
            } 
        } 
        close(fd); 
        exit(0); 
    } 
    

为了能够较好地观察运行结果,需要把这两个程序分别在两个终端里运行,首先启动读管道程序。读管道进程在建立管道之后就开始循环地从管道里读出内容,如果没有数据可读,则一直阻塞到写管道进程向管道写入数据。在启动了写管道程序后,读进程能够从管道里读出用户的输入内容。

信号

信号是 UNIX 中所使用的进程通信的一种最古老的方法。它是在软件层次上对中 断机制的一种模拟,是一种异步通信方式。具体请参见Linux中的信号机制


信号量

多个进程可能为了完成同一个任务会相互协作,这样形成进程之间的同步 关系。而且在不同进程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞 争状态,这就是进程之间的互斥关系。

进程之间的互斥与同步关系存在的根源在于临界资源。临界资源是在同一个时刻 只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源,通常包括 硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段, 共享结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会成为临界资源。

信号量是用来解决进程之间的同步与互斥问题的一种进程之间通信机制,包括一个 称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个 原子操作(PV 操作)。其中信号量对应于某一种资源,取一个非负的整型值。信号量值 指的是当前可用的该资源的数量,若它等于 0 则意味着目前没有可用的资源。

PV 原子 操作的具体定义如下:

P 操作:如果有可用的资源(信号量值>0),则占用一个资源(给信号量值减去 一,进入临界区代码);如果没有可用的资源(信号量值等于 0),则被阻塞到,直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。

V 操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程。 如果没有进程等待它,则释放一个资源(给信号量值加一)。

使用信号量访问临界区的伪代码所下所示:

{ 
     /* 设 R 为某种资源,S 为资源 R 的信号量*/ 
     INIT_VAL(S); /* 对信号量 S 进行初始化 */ 
     非临界区; 
     P(S); /* 进行 P 操作 */ 
     临界区(使用资源 R); /* 只有有限个(通常只有一个)进程被允许进入该区*/ 
     V(S); /* 进行 V 操作 */ 
     非临界区; 
}

最简单的信号量是只能取 0 和 1 两种值,这种信号量被叫做二维信号量。二维信号量的应用比较容易地扩展到使用多维信号量的情况。

  • 作用:保护共享资源(同步/互斥),本质就是一个计数器。

    • 互斥是指每个进程需要先获取到信号量才可以访问临界资源,如果获取失败,就会阻塞等待。
    • 同步是指某进程在某个点试图获取信号量(阻塞操作),即等待另一个进程完成某项工作到达某一个同步点的时候释放得信号量。
  • 用法

    1. 定义一个唯一的key(ftok)
    2. 构造一个信号量(semget)。此时需要调用 semget()函数,不同进程通过使用同一个信号量键值来获得同一个信号量。
    3. 初始化信号量(semctl + SETVA)。此时使用 semctl()函数的 SETVAL 操作。当使用二维信号量 时,通常将信号量初始化为 1。
    4. 对信号量进行PV操作(semop)。此时调用 semop()函数。这一步是实现进程之间的同 步和互斥的核心工作部分。
    5. 删除信号量(semctl + RMID)。此时使用 semclt()函数的 IPC_RMID 操作。此时需要注意,在程序中不应该出现对已经被删除的信号量的操作。
  • 函数介绍

    • 所在头文件:<sys/types.h>、<sys/ipc.h>、<sys/sem.h>
    1. semget()

      • 功能:获取信号量对象的ID
      • 函数原型int semget(key_t key, int nsems, int semflg);
        • key:信号量键值,多个进程可以通过它访问同一个信号量,其中有个特殊值 IPC_PRIVATE,它用于创建当前进程的私有信号量。
        • nsems:需要创建的信号量数量,通常取值为 1。
        • semflg:
          • 同 open()函数的权限位,也可以用八进制表示法。
          • IPC_CREAT:信号量不存在则创建,即使该信号量已经存在也不会出错。
          • IPC_EXCL:与IPC_CREAT标志同时使用,表示创建一个新的唯一的信号量,此时如果该信号量已经存在,该函数会返回出错。
        • 返回值:
          • 成功:信号量ID,在信号量的其他函数中都会使用该值。
          • 失败:-1
    2. semctl()

      • 功能:获取/设置/删除信号量的相关属性

      • 函数原型int semctl(int semid, int semnum, int cmd, union semun arg);

        • semid:semget()函数返回的信号量ID

        • semnum:信号量编号,当使用信号量集时才会被用到。通常取值为 0,就是使用单个信号量(也是第一个信号量)。

        • cmd:指定对信号量的各种操作,当使用单个信号量(而不是信号量集)时,常 用的有以下几种

          • IPC_STAT:获取信号量(信号量集)的属性信息(semid_ds 结构),并存放在由第 4 个参数 arg 的 buf 指向的 semid_ds 结构中。
          • IPC_RMID:从系统中删除信号量(信号量集)
          • IPC_SETVAL:设置信号量的值,即将信号量值设置为 arg 的 val 值。
          • IPC_GETVAL:返回信号量当前值。
        • arg:一种union semnn 结构,该结构可能在某些系统中并不给出定义,此时必须由 程序员自己定义:

          union semun
          {
              int val;
              struct semid_ds *buf;
              unsigned short *array;
          }
          
        • 返回值:

          • 成功:根据 cmd 值的不同而返回不同的值,其中 IPC_STAT、IPC_SETVAL、IPC_RMID:返回 0, IPC_GETVAL返回信号量的当前值
          • 失败:-1
    3. semop()

      • 功能:信号量的PV操作函数

      • 函数原型int semop(int semid, struct sembuf *sops, size_t nsops);

        • semid:semget()函数返回的信号量ID

        • sops:指向信号量操作结构体数组,结构体定义如下:

          struct sembuf 
          { 
              /* 信号量编号,使用单个信号量时,通常取值为 0 */
              short sem_num;
              
              /* 信号量操作:取值为-1 则表示 P 操作,取值为+1 则表示 V 操作*/ 
              short sem_op; 
              
              /* 通常设置为 SEM_UNDO。这样在进程没释放信号量而退出时,系统自动
              释放该进程中未释放的信号量 */ 
              short sem_flg; 
          } 
          
        • nops:操作数组 sops 中的操作个数(元素数目),通常取值为 1(一个操作)。

        • 返回值:

          • 成功:信号量ID
          • 失败:-1
  • 函数封装

    因为信号量相关的函数调用接口比较复杂,我们可以将它们封装成二维单个信号量的几个基本函数。它们 分别为信号量初始化函数init_sem()、P 操作函数 sem_p()、V 操作函数 sem_v()以 及删除信号量的函数 del_sem()等,具体实现如下所示:

    //sem_api.c	;以后在头文件"sem_api.h"中包含这些接口
    #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;		/* init_value 为初始值 */
        if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
        {
            perror("initialize semaphor");
            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;
    }
    
    
**示例代码**:首先创建一个子进程,接下来使用信号量来控制 两个进程(父子进程)之间的执行顺序。
    
    ```c
    //test.c
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <sys/ipc.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((ftok(".", 'a'), 1, 0666|IPC_CREAT);	//创建一个信号量
        init_sem(sem_id);
                        
        pid = fork();
        if(pid == -1)
        {
            perror("fork");
        }
        else if(pid == 0)
        {		 /*返回值为 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
        {					/*返回值大于 0 代表父进程*/
        sem_p(sem_id);	//等待子进程执行完以获取信号量
            printf("the parent process is running\n");
        sem_v(sem_id);       
            del_sem(sem_id);
        }
         return 0;    
    }
> 关于ftok函数,先不去了解它的作用来先说说为什么要用它,共享内存,消息队列,信号量它们三个都是找一个中间介质,来进行通信的,这种介质多的是。就是怎么区分出来,就像唯一一个身份证来区分人一样。你随便来一个就行,就是因为这。只要唯一就行,就想起来了文件的设备编号和节点,它是唯一的,但是直接用它来作识别好像不太好,不过可以用它来产生一个号。ftok()就出场了。ftok函数具体形式如下:
>
>   key_t ftok(const char *pathname, int proj_id);
>
>   其中参数fname是指定的文件名,这个文件必须是存在的而且可以访问的。id是子序号,它是一个8bit的整数。即范围是0~255。当函数执行成功,则会返回key_t键值,否则返回-1。在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值。

运行结果:

```sh
$ ./test
Child process will wait for some seconds… /*子进程运行中,父进程等待其结束*/ 
the child process is running... /* 子进程结束了*/ 
the parent process is running... /* 父进程结束*/ 
```


共享内存

共享内存是一种最为高效的进程间通信方式。因为进程可以直接读写内存,不需要任何数据 的复制。为了在多个进程间交换信息,内核专门留出了一块内存区。这段内存区可以由需要访问的进 程将其映射到自己的私有地址空间。因此,进程就可以直接读写这一内存区而不需要进行数据的复制, 从而大大提高了效率。当然,由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁 和信号量等(请参考本章的共享内存实验)

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

    image-20220417214249348

  • 用法

    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:共享对象键值(标识符),多个进程可以通过它访问同一个共享内存,其中有个特 殊值 IPC_PRIVATE。它用于创建当前进程的私有共享内存。
          • size:共享内存大小
          • shmflg:属性信息
            • IPC_CREAT:若共享内存不存在则新建,否则返回该共享内存的ID。
            • IPC_CREAT | IPC_EXCL:若共享内存存在则报错,若不存在就新建,意思就是说返回的一定是新建的共享内存ID。
            • mode:新创建的共享内存权限,同 open()函数的权限位,也可以用八进制表示法,与前面的属性相或。
          • 返回值:
            • 成功:共享内存ID
            • 失败:-1
    2. shmat()

      • 功能将共享内存映射到进程的虚拟空间
      • 函数原型
        • void* shmat(int shmid, const void *shmaddr, int shmflg);
          • shmid:要映射的共享内存区ID
          • shmaddr:共享内存的映射地址,(若为 0 则表示系统自动分配地址并把该段共享内存映射到调用进程的地址空间)
          • shmflg:以什么权限将共享内存附加到进程当中
            • SHM_RDONLY:只读方式映射
            • 0:默认值,以可读写方式映射
          • 返回值:
            • 成功:映射到的虚拟地址
            • 失败:(void *)-1
    3. shmdt()

      • 功能解除共享内存映射
      • 函数原型
        • int shmdt(const void *shmaddr);
          • shmaddr:共享内存的映射地址(虚拟地址)
          • 返回值:
            • 成功:0
            • 失败:-1
    4. 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;	//连接数
                    ...
                }
              
          • image-20220417220612241

          • 返回值:成功返回值由cmd类型决定,失败返回-1

  • 特性

    • 覆盖写:向共享内存写数据时,会覆盖旧数据(清空共享内存区)

    • 反复读:从共享内存读数据时,不会影响旧数据(区别于字节流的管道)

    • 可通过命令行删除现有共享内存:

      • 执行ipcs查看系统当前进程间通信情况(消息队列、共享内存、信号量)

        image-20220417221748821

      • 执行ipcrm -m shmid删除id为shmid的共享内存

        image-20220417232256849

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

          image-20220417234203171

  • 代码示例

    1. 两个进程(以父子进程为例,但也可以是非父子进程)分别通过shmget函数创建/获取共享内存,且这两个进程在创建/获取共享内存时,其参数设置(大小)必须一致,否则获取到就不是同一个共享内存。
    2. 分别调用shmat接口将共享内存附加到自己的进程虚拟地址空间去。
    3. 进程间通信,并使用信号量进行同步。
    4. 通信结束后将进程和共享内存分离。
    //test.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include "sem_api.h"	//包含了之前在信号量一节中自定义的接口函数
    #include <sys/shm.h>
    
    #define DELAY_TIME 3
    #define BUFFER_SIZE 1024
    int main(void)
    {
        pid_t pid;
        int sem_id;
        int shm_id;
        char *addr;
        char buff[BUFFER_SIZE+1]={};
        
        //创建一个信号量
        if((sem_id = semget((key_t)6666, 1, 0666|IPC_CREAT)) < 0)
        {
            perror("semget");
            exit(1);
        }
        else
        {
            printf("Create semphore: %d\n",sem_id); 
        }
        init_sem(sem_id,0);		/*初始化信号量*/
        
        if((shm_id = shmget((key_t)7777, BUFFER_SIZE, 0666|IPC_CREAT)) < 0)	//创建共享内存对象
        {
            perror("shmget");
            exit(1);     
        }
        else
        {
            printf("Create shared-memory: %d\n",shm_id); 
        }
        
        system("ipcs -m"); 		//显示共享内存情况
        
    
        /*调用fork()函数*/
        pid = fork();
        if(pid == -1)
        {
            perror("fork failed\n");
            exit(1);
        }
        else if(pid == 0)		//子进程
        {
            /*映射可读写的共享内存*/
            addr = shmat(shm_id, NULL, 0);
            if(addr == (void *)-1)
            {
                perror("Child: shmat");
                exit(1);
            }
            else
            {
                printf("Child: Attach shared-memory: %p\n", addr);
            }
            system("ipcs -m"); 
            
    		/*向共享内存中写入内容*/
            printf("\nInput some string:\n"); 
     		fgets(buff, BUFFER_SIZE, stdin);
            memcpy(addr, buff,strlen(buff)+1);
            printf("the child process is wroted.\n");
            sem_v(sem_id);			//异步通知父进程,已写入完毕。
            
            /* 解除共享内存映射 */ 
             if ((shmdt(addr)) < 0) 
             { 
                 perror("Child: shmdt"); 
                 exit(1); 
             } 
             else 
             { 
             	printf("Child: Deattach shared-memory\n"); 
             } 
             system("ipcs -m"); 
        }
        else	//父进程
        {
            sleep(DELAY_TIME);	//等待子进程先执行,也可以不写,因为有信号量进行同步
            sem_p(sem_id);	//等待子进程向共享内存中写入数据
            printf("the parent process is running\n");
            /*映射共享内存地址*/
            addr = shmat(shm_id, NULL, 0);
            if(addr == (void *)-1)
            {
                perror("Parent:shmat");
                exit(1);
            }
            else
            {
                printf("Parent: Attach shared-memory: %p\n", addr); 
            }
            system("ipcs -m"); 
            
            /* 获取共享内存的有效数据并显示 */
            printf("shared memory string:%s\n",addr);
            
            /* 解除共享内存映射 */ 
             if ((shmdt(addr)) < 0) 
             { 
                 perror("Child: shmdt"); 
                 exit(1); 
             } 
             else 
             { 
             	printf("Parent: Deattach shared-memory\n"); 
             } 
             system("ipcs -m"); 
            
             /*删除共享内存映射*/
            if(shmctl(shm_id, IPC_RMID, NULL) == -1)
            {
                perror("Parent: shmctl(IPC_RMID)\n"); 
                exit(1); 
            }
            else
            {
                printf("Delete shared-memory\n");
            }
            system("ipcs -m");
            
            waitpid(pid, NULL, 0); 	//子进程回收
            printf("Finished\n"); 
        }
        return 0}
    

消息队列

消息队列就是一些消息的列表。用户可以从消息队列中添加消息和读取消息等。从这点上看, 消息队列具有一定的 FIFO 特性,但是它可以实现消息的随机查询,比 FIFO 具有更大的优势。同时,这些 消息又是存在于内核中的,由“队列 ID”来标识。

由内核维护消息的链表,系统中存在多个msgqueue,并用消息队列ID(qid)唯一区分。在进行进程间通信的时候,一个进程将消息追加到消息队列的尾端,另一个进程从消息队列里取走数据,但不一定严格按照先进先出的原则取数据,也可以按照消息类型字段来取。

消息队列的实现包括创建或打开消息队列、添加消息、读取消息和控制消息队列这 4 种操作。其中创建或打开消息队列使用的函数是 msgget(),这里创建的消息队列的数量会受到系统消息队列数量的限制; 添加消息使用的函数是 msgsnd()函数,它把消息添加到已打开的消息队列末尾;读取消息使用的函数是 msgrcv(),它把消息从消息队列中取走,与 FIFO 不同的是,这里可以指定取走某一条消息;最后控制消 息队列使用的函数是 msgctl(),它可以完成多项功能。若想使用它们,需在程序中包含<sys/types.h>、<sys/ipc.h>、<sys/shm.h>这3个头文件。下面分别看看这4种函数。

  • 4种接口函数

    • int msgget(key_t key, int msgflg);

      • key:消息队列的标识符,多个进程可以通过它访问同一个消息队列,其中有个特殊值 IPC_PRIVATE,它用于创建当前进程的私有消息队列。
      • msgflg:权限标志
        • IPC_CREAT:若不存在则新建;
        • mode:按位或上权限(八进制数,如0664)
      • 返回值:
        • 成功:消息队列的ID(qid)
        • 失败:-1
    • int msgsnd(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
    • int msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

      • msgid:消息队列的ID(qid)
      • msgp:指向待接收消息的结构体(struct msgbuf)的指针;
      • msgsz:要接收消息的长度(就是指msgbuf结构体中char mtext[]数组的长度)
      • msgtyp:
        • 等于0:读取队列中第一条消息
        • 大于0:读取队列中类型为msgtyp的第一条消息,但如果指定了MSG_EXCEPT,则读取类型为非msgtyp的第一条消息。
        • 小于0:接收消息队列中第一个类型值不小于 msgtyp 绝对值 且类型值又最小的消息
      • msgflg:创建标记
        • 0:阻塞接收
        • IPC_NOWAIT:非阻塞接收
        • MSG_NOERROR:若返回的消息比 msgsz 字节多,则消息就 会截短到 msgsz 字节,且不通知消息发送进程。
      • 返回值:
        • 成功:返回实际读取消息的字节数
        • 失败:-1
    • int msgctl(int msgid, int cmd, struct msgid_ds *buf);

      • msgid:消息队列的ID(qid)
      • cmd:控制命令
        • IPC_RMID:从系统内核中删除消息队列
        • IPC_STAT:读取消息队列的数据结构 msqid_ds,并将其存储在 buf 指定的地址中。
        • IPC_SET:设置消息队列的数据结构 msqid_ds 中的 ipc_perm 域 (IPC 操作权限描述结构)值。这个值取自 buf 参数。
      • buf:存储消息队列的相关信息
      • 返回值:
        • 成功:0
        • 失败:-1
  • 用法

    1. msgget函数新建/获取一个消息队列;
    2. 定义一个消息缓冲区msgbuf,用来发送或接收信息
    3. 对消息队列进行发送(msgsnd)或接收(msgrcv
    4. 删除消息队列(什么周期跟随系统)。
  • 代码实例:一个进程发送,一个进程接收。注意:不同进程间使用消息队列进行通信的时候,需要获取相同的消息队列标识符。

    • 以下是消息队列发送端的代码:
    //msg_send.c
    #include <sys/types.h> 
    #include <sys/ipc.h> 
    #include <sys/msg.h> 
    #include <stdio.h> 
    #include <stdlib.h> 
    #include <unistd.h> 
    #include <string.h>
    
    #define BUFFER_SIZE  256
    
    struct msgbuf
    {
        long mtype;
        char mtext[BUFFER_SIZE];
    };
    
    int main(void)
    {
        int qid;
        key_t key;
        struct msgbuf msg;
        
        /*根据不同的路径和关键字产生标准的 key*/
        if ((key = ftok(".", 'a')) == -1) 
         { 
             perror("ftok"); 
             exit(1); 
         } 
        
        /*创建消息队列*/
        if(qid = msgget(key, IPC_CREAT|0664) == -1)	
        {
            perror("msgget");
            exit(1);
        }
    	printf("queue ID is %d\n", qid);
        
         /* 初始化消息的buffer */
        while(1)
        {
            printf("Enter some message to the queue:");         
            if ((fgets(msg.mtext, BUFFER_SIZE, stdin)) == NULL) 
            { 
            	puts("no message"); 
                exit(1);
            }
            msg.mtype = getpid();		//指定消息类型为进程号
            
            /*发送消息*/
        	if((msgsnd(qid, &msg, sizeof(msg.mtext), 0)<0)
        	{
                perror("msgsnd");
                exit(1);
        	}
               
            if (strncmp(msg.mtext, "quit", 4) == 0) 
            { 
            	break; 
            }
         }
         return 0;
    }
    
    • 以下是消息队列接收端的代码:
    //msg_receive.c
    #include <sys/types.h> 
    #include <sys/ipc.h> 
    #include <sys/msg.h> 
    #include <stdio.h>
    #include <stdlib.h> 
    #include <unistd.h> 
    #include <string.h> 
    
    #define BUFFER_SIZE 256 
    
    struct msgbuf
    {
        long mtype;
        char mtext[BUFFER_SIZE];
    };
    
    int main(void)
    {
    	key_t key;
        int qid;
        struct msgbuf msg;
        /*根据不同的路径和关键字产生标准的 key*/ 
        if ((key = ftok(".", 'a')) == -1) 
        { 
            perror("ftok"); 
            exit(1); 
        }
        
        /*创建消息队列*/
        if((qid = msgget(key, IPC_CREAT|0664)) == -1)
        {
            perror("msgget");
            exit(1);
        }
        printf("queue ID is %d\n", qid);
        
        do
        {
            /*接收消息*/   
            memset(msg.mtext, 0, BUFFER_SIZE);
            if(msgrcv(qid, (void *)&msg, BUFFER_SIZE, 0, 0) < 0)
            {
                perror("msgrcv");
                exit(1);
            }
            /*打印*/
            printf("The message from process %ld : %s", \
                   msg.mtype, msg.mtext);
        }while(strncmp(msg.msg_text, "quit", 4));
        
        /*从系统内核中移走消息队列 */ 
     	if ((msgctl(qid, IPC_RMID, NULL)) < 0) 
        { 
            perror("msgctl"); 
            exit(1); 
        }
        return 0;
    }
    
    • 以下是程序的运行结果。输入“quit”则两个进程都将结束。

      $ ./msg_send 
      Open queue 65536
      Enter some message to the queue:first message 
      Enter some message to the queue:second message 
      Enter some message to the queue:quit 
      $ ./msg_receive 
      Open queue 65536 
      The message from process 6072 : first message 
      The message from process 6072 : second message 
      The message from process 6072 : quit 
      
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值