zoukankan      html  css  js  c++  java
  • 笔记:Linux进程间通信机制

    Linux支持管道、信号、unix system V三种IPC(Inter-Process-Communication)机制。以下分别对三种机制加以简单介绍。

    一、信号机制:

    信号又称作软中断,用来通知进程发生了异步事件;主要用于向一个或多个进程发异步事件信号,信号可以通过键盘中断触发、当进程访问虚拟内存中不存在的线性地址或者禁止访问的地址时也会触发。也可以用于shell作业任务时向子进程发送控制命令。

    Linux系统共有30多种信号,每个信号名称都以 SIG 三个字符开头,例如异常终止信号名为 SIGABRT。在头文件<signal.h>中,这些信号都被定义为正整数。以下为常用信号:

     

    Linux 可以使用 kill 命令向指定进程发送指定信号,kill命令的用法为:

    kill [选项]PID

    进程可以选择忽略上面的大多数信号,但和SIGKILL是不可忽略的。其中SIGSTOP信号,使进程停止执行;而SIGKILL信号使进程中止。对于其他情况,进程可以自主决定如何处理各种信号:它可以阻塞信号;如果不阻塞,也可以选择由进程自己处理信号或者由内核来处理。由内核来处理信号时,内核对每个信号使用相应的缺省处理动作,例如:当进程收到SIGFPE信号(浮点异常)时,内核的缺省动作是进行内核转贮 (core dump),然后中止该进程。

    信号之间不存在内在的相对优先级。如果对同一个进程同时产生两个信号的话,它们会按照任意顺序提交给该进程,并且对同种信号无法区分信号的数量。

    Linux使用存贮在每个进程task_struct结构中的信息实现信号机制它支持的信号数受限于处理器的字长,具有32位字长的处理器有32种信号,64位字长的处理器最多有64种信号,task_struct如下所示:

    struct task_struct {

             volatile long state;  /* -1 unrunnable, 0 runnable, >0 stopped */

             void *stack;

             atomic_t usage;

             unsigned int flags;   /* per process flags, defined below */

             unsigned int ptrace;

             int lock_depth;                   /* BKL lock depth */        

      …

      /* signal handlers */

      struct signal_struct *signal;

      struct sighand_struct *sighand;

             …

    }

    当前未处理的信号记录在signal域中,并把阻塞信号掩码对应位设置为阻塞状态。但对 SIGSTOP和SIGKILL信号来说,所有的信号都被设置为阻塞状态。如果一个被阻塞的信号产生了,就将一直保持未处理状态,直到阻塞被取消。

    并不是系统中的每个进程都可以向其他的进程发消息,只有内核和超级用户可以做到这一点。普通的进程只能向同一进程组或具有相同的uid和gid的进程发送信号。信号可以通过设置task_struct结构signal域中相应中的位来产生。如果一个进程没有阻塞信号,正处于可中断的等待信号状态中,当等待的信号出现时 ,系统可以通过把该进程的状态变成运行状态 ,然后放入候选运行队列中的方法来唤醒它。通过上面的方法在下次调度时 ,进程调度器会把该进程作为候选运行进程进行调度。

    Linux兼容POSIX标准,进程在某个信号处理例程被调用时,能指出哪些信号可以被阻塞。这意味着在调用进程信号处理例程时需要改变阻塞掩码,当信号处理例程结束时,阻塞掩码必须要恢复到初始值。因此linux增加了一次对清理例程的调用。清理例程按照信号处理例程的调用栈来恢复初始的阻塞掩码。在几个信号处理例程都需要被调用时,linux也提供了优化方案。linux把这些例程压入栈中,这样每当一个处理例程退出时,下一个处理例程立即被调用,当所有处理例程都完成后清理例程被调用。

    二、管道机制:

    管道是一个进程连接数据流到另一个进程的通道, 它通常是用作把一个进程的输出通过管道连接到另一个进程的输入。在 Linux 命令中通常通过符号“ |”来使用管道,例如:

    $ ps -ef | grep init

    此命令中 ps 是一个独立的进程, grep 也是一个独立的进程,中间的管道把本来要输出到屏幕的数据输出到 grep 这个进程中,作为 grep 这个进程的输入。

    在Linux系统中,管道用两个指向同一个临时性VFS索引节点的文件数据结构来实现。这个临时性的VFS索引节点指向内存中的一个物理页面。下图表明每个文件数据结构包含指向不同文件操作例程向量的指针。一个例程用于写管道,另一个用于从管道中读数据。从一般读写普通文件的系统调用的角度来看,这种实现方法隐藏了下层的差异。当写进程执行写管道操作时,数据被复制到共享的数据页面中 ;而读进程读管道时,数据又从共享数据页中复制出来。Linux必须同步对管道的访问,使读进程和写进程步调一致。为了实现同步, Linux使用锁、等待队列和信号量这三种方式。

     

    管道分为匿名管道和命名管道两种,匿名管道主要用于两个进程间有父子关系的进程间通信,命名管道主要用于没有父子关系的进程间通信。

    2.1 匿名管道

    匿名管道是不能在文件系统中以任何方式看到的半双工管道。半双工管道意味着管道的一端只读或只写。父子进程间匿名管道通信示意图如图:

     

    2.2命名管道

    命名管道也被称为 FIFO 文件, 它突破了匿名管道无法在无关进程之间通信的限制,使得同一主机内的所有的进程都可以通信。同时命名管道是一个特殊的文件类型, 它在文件系统中以文件名的形式存在,在stat结构中 st_mode 指明一个文件结点是不是命名管道。

    struct stat {

             unsigned long  st_dev;              /* Device.  */

             unsigned long  st_ino;               /* File serial number.  */

             unsigned int   st_mode;                  /* File mode.  */

    };

    mkfifo()函数用来创建一个命名管道,它的原型如下:

    int mkfifo(const char *pathname, mode_t mode);

    mkfifo()创建一个真实存在于文件系统中的命名管道文件,参数 pathname指定了文件名,参数 mode 则指定了文件的读写权限。 函数成功返回 0,否则返回-1 并设置 errno。

    mkfifo()创建命名管道文件后,需要通过命名管道通信的进程需要打开该管道文件,然后通过 read、 write 函数像操作普通文件一样进行通信。

    三、System V机制:

    System V是Unix中出现最早的三种进程间通信机制,linux也支持;它们是消息队列、信息量和共享存储器。这些 System V的进程间通信机制使用相同的认证方法,即通过系统调用向内核传递这些资源的全局唯一标识来访问它们,linux使用访问许可的方式核对对 System V-IPC对象的访问,这种方式与文件访问权限的检查十分相似。

    System V IPC对象的访问权限是由该对象的创建者通过系统调用来实现的。 linux的每种IPC机制都把 IPC对象的访问标识作为对系统资源表的索引 ,但访问标识不是一种直接的索引,而是由索引标识通过某些运算来产生的对象索引。

    Linux系统中所有代表 System V IPC 对象的数据结构中都包括ipc_perm数据结构,在ipc_perm结构中有拥有者和创建者进程的用户标识和组标识、该对象的访问模式以及 IPC对象的密钥。密钥的用处是确定 System V IPC 对象的索引标识。 Linux系统中支持两种密钥:公钥和私钥。如果 IPC对象的密钥是公共的,那么系统中的进程在通过权限检查后就可以得到System V IPC对象的索引标识。但要注意 System V IPC对象不是通过密钥而是通过它们的索引标识来访问的。

    以下为ipc_perm数据结构:

    struct ipc_perm

    {

             __kernel_key_t        key;

             __kernel_uid_t        uid;

             __kernel_gid_t        gid;

             __kernel_uid_t        cuid;

             __kernel_gid_t        cgid;

             __kernel_mode_t    mode;

             unsigned short         seq;

    };

    3.1 消息队列

    消息队列允许一个或多个进程向队列中写入消息, 然后由一个或多个读进程读出(见下图)。Linux系统维护一个消息队列的表。该表是msqid_ds结构的数组,数组中每个元素指向一个能完全描述消息队列的msqid_ds数据结构。一旦一个新的消息队列被创建,则在系统内存中会为一个新的msqid_ds数据结构分配空间,并把它插入到数组中。每个msqid_ds结构都包含ipc_perm数据结构以及指向进入该队列的消息的指针。除此之外,Linux还记录像队列最后被更改的时间等队列时间更改信息。 msqid_d结构还包括两个等待队列;一个用于存放写进程的消息,另一个用于消息队列。

    每次进程要向写队列写入消息时,系统都要把它的有效用户标识和组标识与该队列的ipc_perm数据结构中的访问模式进行比较。如果进程可以写队列,那么消息会从进程的地址空间复制到一个 msg数据结构中,然后系统把该msg数据结构放在消息队列的尾部。由于Linux限制写消息的数量和消息的长度,所以可能会出现没有足够的空间来存放消息的情况。这时当前进程会被放入对应消息的写等待队列中,系统调用进程调度器选择合适的进程运行。在该消息队列中有一个或多个消息被读出时,睡眠的进程会被唤醒。

     struct msqid_ds {

             struct ipc_perm msg_perm;

             struct msg *msg_first;             /* first message on queue,unused  */

             struct msg *msg_last;              /* last message in queue,unused */

             unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */

             unsigned long  msg_lqbytes;         /* ditto */

             unsigned short msg_cbytes;   /* current number of bytes on queue */

             unsigned short msg_qnum;     /* number of messages in queue */

             unsigned short msg_qbytes;   /* max number of bytes on queue */

             __kernel_ipc_pid_t msg_lspid;        /* pid of last msgsnd */

             __kernel_ipc_pid_t msg_lrpid;        /* last receive pid */

    };

    从队列中读消息的过程与前面相似,进程对写队列的访问权限会再次被核对。一个读进程可以选择获得队列中的第一个消息而不考虑消息的类型,还是读取某种特别类型的消息。如果没有符合要求的消息的话,读进程会被加入到该消息的读等待队列中,系统唤醒进程调度器调度新进程运行。一旦有新消息被写入消息队列。睡眠的进程被唤醒,并再次运行。

    3.2 信号量

    多进程编程中需要关注进程间同步及互斥。同步是指多个进程为了完成同一个任务相互协作运行,而互斥是指不同的进程为了争夺有限的系统资源(硬件或软件资源)而相互竞争运行。

    信号量是用来解决进程间的同步与互斥问题的一种进程间通信机制,它是一个特殊的变量,变量的值代表着关联资源的可用数量。信号量只能进行的两个原子操作: P 操作: V 操作。

    P 操作:如果有可用的资源(信号量值>0),则占用一个资源(给信号量值减 1);如果没有可用的资源(信号量值=0),则进程被阻塞直到系统将资源分配给该进程(进入信号量的等待队列,等到资源后唤醒该进程)。

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

    System V的每个IPC信号量对象都对应一个信号量数组,在 Linux中用semid_ds数据结构来表示它,如下:

    struct semid_ds {

             struct ipc_perm       sem_perm;                /* permissions .. see ipc.h */

             __kernel_time_t      sem_otime;               /* last semop time */

             __kernel_time_t      sem_ctime;               /* last change time */

             struct sem        *sem_base;              /* ptr to first semaphore in array */

             struct sem_queue *sem_pending;          /* pending operations to be processed */

             struct sem_queue **sem_pending_last;        /* last pending operation */

             struct sem_undo     *undo;                        /* undo requests on this array */

             unsigned short         sem_nsems;             /* no. of semaphores in array */

    };

    系统中所有的semid_ds数据结构都被一个叫semary的指针向量指向。在每个信号量数组中都有 sem_nsems域,这个域由sem_base指向的sem数据结构来描述。所有允许对System V IPC信号量对象的信号量数组进行操作的进程,都必须通过系统调用来执行这些操作。在系统调用中可以指出有多少个操作。而每个操作包含三个输入项:信号量的索引、操作值和一组标志位。信号量索引是对信号量数组的索引值,而操作值是加到当前信号量值上的数值。首先Linux会测试是否所有的操作都会成功 (操作成功指操作值加上信号量当前值的结果大于 0,或者操作值和信号量的当前值都是 0 )。如果信号量操作中有任何一个操作失败, Linux在操作标志没有指明系统调用为非阻塞状态时,会挂起当前进程。如果进程被挂起了,系统会保存要执行的信号量操作的状态,并把当前进程放入等待队列中。通过在栈中建立一个sem_queue数据结构,并填入相应的信息的方法来实现前面的保存信号量操作状态的。新的 sem_queu 数据结构被放在对应信号量对象的等待队列的末尾 ( 通过使用sem_pending和sem_pending_lastt指针),当前进程被放在 sem_queue数据结构的等待队列中,然后系统唤醒进程调度器选择其他进程执行。

     

    信号量存在着死锁的问题,当一个进程进入了关键段,改变了信号量的值后,由于进程崩溃或被中止等原因而无法离开关键段时,就会造成死锁。 Linux通过为信号量数组维护一个调整项列表来防止死锁。主要的想法是在使用调整项后,信号量会被恢复到一个进程的信号量操作集合执行前的状态。调整项被保存在sem_undo数据结构中,而这些sem_undo数据结构则按照队列的形式放在 semid_ds数据结构和进程使用信号量数组的task_struct数据结构中。

    当进程被删除时,退出时Linux会用这些sem_undo数据结构集合对信号量数组进行调整。如果信号量集合被删除了,那么这些sem_undo数据结构还存在于进程的sem_undo结构的队列中,而仅把信号量数组标识标记为无效。在这种情况下,信号量清理程序仅仅丢掉这些数据结构而不释放它们所占用的空间。

    3.3 共享内存

    共享内存是允许两个不相关的进程访问同一个逻辑内存的进程间通信方法,是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。

    不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用 C语言 malloc()分配的内存一样。两个进程使用共享内存通信机制如下图所示。

     

    共享存储区不需要在所有进程的虚存中占有相同的虚地址。像所有的 System V IPC对象一样,共享存储区的访问控制是通过密钥和访问权限检查来实现的。一旦某一内存区域被共享了,系统就无法检查进程如何使用这部分内存区域。因此系统必须使用信号量等其他的机制来同步对存储器的访问。

  • 相关阅读:
    Spring Boot 常用注解
    python类的理解
    深入理解JavaScript的执行机制(同步和异步)
    HBuilderX scss/sass 使用教程
    uniapp引入微信小程序直播组件
    常见正则表达式例子
    远程桌面提示:身份验证错误 要求的函数不受支持
    ORCAL使用中存在的问题记录
    SQLSERVER常用函数
    vue-router.esm.js: Error: "Loading chunk 0 failed"
  • 原文地址:https://www.cnblogs.com/zhangyi-studio/p/7900965.html
Copyright © 2011-2022 走看看