《深入理解linux内核》学习 — 进程通信

进程相关最后一个章节。

通过创建一个文件并使用适当的VFS系统调用对该文件加锁和解锁就可以在用户态进程之间实现某种同步,但是访问磁盘文件的代价很高。Unix内核包含了一组系统调用,这些系统调用不用与文件系统打交道就可以支持进程通信。一次介绍Uinx系统提供的进程间通信的基本机制。

管道

管道(pipe)是所有Uinx都愿意提供的一种进程间通信机制。管道是进程之间的一个单向数据流:一个进程写入管道的所有数据都由内核定向到另一个进程,另一个进程由此就可以从管道中读取数据。

管道可以被看作是打开的文件,但在已安装的文件系统中没有相应的映像。可以使用pipe()系统调用来创建一个新莞岛,这个系统调用返回一对文件描述符(读取与写入);然后通过fork()把这两个描述符传递给它的子进程,由此与子进程共享管道。进程可以在read()系统调用中使用第一个文件描述符从管道中读取数据,也可以是用write()系统调用从第二个文件描述符向管道写入数据。

POSIX是定义了半双工管道,因此使用pipe()系统调用返回了两个描述符,每个进程在使用一个描述符之前要把另一个描述符关闭。如果需要的是双向数据流,那么进程必须通过两次调用pipe()来使用两个不同管道。Linux内为了实现全双工,在使用一个描述符之前不必把两一个描述符关闭。

例子

一个shell命令”ls | more”,在Uinx中使用”|”创建管道,此处第一个进程执行ls程序,他的标准输出被重定向管道中,第二个进程执行more程序,从这个管道中读取输入。完整行为如下:

  1. 调用pipe()系统调用,假设pipe()返回文件描述符3(读通道)与4(写通道)
  2. 两次调用fork()系统调用
  3. 两次调用close()系统调用来释放文件描述符3和4

第一个子进程执行ls程序:

  1. 调用dup2(4,1)把文件描述符4拷贝到文件描述符1
  2. 两次调用close()系统调用来释放文件描述符3和4
  3. 调用execve()系统调用来执行ls程序,缺省情况下,这个程序要把自己的输出写到文件描述符为1的那个文件

第二个子进程执行more程序:

  1. 调用dup2(3,0)把文件描述符3拷贝到文件描述符0
  2. 两次调用close()系统调用来释放文件描述符3和4
  3. 调用execve()系统调用来执行ls程序,缺省情况下,这个程序要从文件描述符为0的那个文件中读取输入

管道节点的数据结构是pipe_inode_info,具体成员变量可以结合注释了解,有个成员是bufs,是一个元素个数默认是16的数组,元素类型是pipe_buffer,每个元素代表一个管道缓冲区。每个管道都有自己的管道缓冲区,都是一个个页,16个缓冲区可以看作是一个环形的缓冲区,写进程不断向这个大缓冲区追加数据,读进程不断移出数据。

FIFO

管道有一个主要的缺点,无法打开已经存在的管道。这就使得任意的两个进程不可能共享一个管道,除非管道由一个共同的祖先进程。

为了解决非亲属进程间通信这一问题,Linux提供了FIFO方式连接进程。本质上就是有了磁盘索引节点,所以任何进程都可以访问。

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

FIFO又叫做命名管道(named PIPE),之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。

System V IPC

IPC是进程间通信的缩写,通常指允许用户态进程执行下列操作的一组机制:

  • 通过信号量与其他进程同步
  • 向其他进程发送消息或者从其他进程接收消息
  • 和其它进程共享一段内存区

每个IPC资源都是持久的,除非被显示释放,否则永远驻留在内存中(指导系统关闭),一个进程可以使用多个IPC资源,当两个或多个进程要通过一个IPC资源进行通信时,这些进程都要引用该资源的IPC标识符。

根据资源是信号量、消息队列还是共相内存区,分别调用semget()、msgget()、shmget()函数创建IPC资源,x86体系结构中,底层的系统调用的都是ipc()。

IPC信号量

与内核信号量非常类似:二者都是计数器,用来为多个进程的数据结构提供受控访问。

受保护资源可用时,信号量的值就是正数;受保护资源不可用时,信号量的值就是0。

每个IPC信号量都是一个或者多个信号量值得集合。

Linux内部用一个全局变量sem_ids来存放IPC信号量:

static struct ipc_ids sem_ids;
struct ipc_ids {
    int size;
    int in_use;
    int max_id;
    unsigned short seq;
    unsigned short seq_max;
    struct semaphore sem;   
    struct ipc_id* entries;    // 信号量得指针数组
};
struct ipc_id {
    struct kern_ipc_perm* p;
};
/* One sem_array data structure for each set of semaphores in the system. */
struct sem_array {
    struct kern_ipc_perm    sem_perm;   /* permissions .. see ipc.h */
    time_t          sem_otime;  /* last semop time */
    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 long       sem_nsems;  /* no. of semaphores in array */
};
/* One semaphore structure for each semaphore in the system. */
struct sem {
    int semval;     /* current value */
    int sempid;     /* pid of last operation */
};

可以看到entry中得元素得第一个成员就是sem_array数据结构中的第一个成员得指针。

这里得undo用于内核撤销给定进程得IPC信号量资源得可撤销操作,正是由于两个链表,使得内核可以有效地处理这些任务:

  • 每个进程链表包含所有的sem_undo数据结构,该结构对应于进程执行了可取消操作的IPC信号量
  • 每个信号量链表包含了所有sem_undo数据结构对应于在该信号量上执行可取消操作的进程

同时还用一个双向链表实现的数据结构sem_queue来标识正在等待数组中得一个或多个信号量进程。

IPC消息

进程彼此之间可以通过IPC消息进行通信。进程产生的每条消息都被发送到一个IPC消息队列中,这个消息一只存放在队列中指导另一个进程将其读走为止。

消息时由固定大小的首部和可变长度的正文组成。

msg_ids全局变量存放IPC消息队列资源类型的ipc_ids数据结构,整体布局和sem_ids类似。

static struct ipc_ids msg_ids;
/* one msg_msg structure for each message */
struct msg_msg {
    struct list_head m_list; 
    long  m_type;          
    int m_ts;           /* message text size */
    struct msg_msgseg* next;
    void *security;
    /* the actual message follows immediately */
};
/* one msq_queue structure for each present queue on the system */
struct msg_queue {
    struct kern_ipc_perm q_perm;
    time_t q_stime;         /* last msgsnd time */
    time_t q_rtime;         /* last msgrcv time */
    time_t q_ctime;         /* last change time */
    unsigned long q_cbytes;     /* current number of bytes on queue */
    unsigned long q_qnum;       /* number of messages in queue */
    unsigned long q_qbytes;     /* max number of bytes on queue */
    pid_t q_lspid;          /* pid of last msgsnd */
    pid_t q_lrpid;          /* last receive pid */
    struct list_head q_messages;
    struct list_head q_receivers;
    struct list_head q_senders;
};

IPC共享内存

最有用的IPC机制时共享内存,这种机制允许两个或多个进程通过把公共数据结构放入一个共享内存区来访问他们。

如果进程要访问这种存放在共享内存区的数据结构,就必须在自己的地址空间增加一个新内存去,它映射与这个共享内存区相关的页框。

shm_ids变量存放IPC共享内存资源类型ipc_ids的数据结构。

static struct ipc_ids sem_ids;
#ifdef __KERNEL__
struct shmid_kernel /* private to the kernel */
{   
    struct kern_ipc_perm    shm_perm;
    struct file *       shm_file;
    int         id;
    unsigned long       shm_nattch;
    unsigned long       shm_segsz;
    time_t          shm_atim;
    time_t          shm_dtim;
    time_t          shm_ctim;
    pid_t           shm_cprid;
    pid_t           shm_lprid;
    struct user_struct  *mlock_user;
};

POSIX消息队列

POSIX标准基于消息队列定义了一个IPC机制,就是大家知道的POSIX消息队列,相比老的队列具有许多优点:

  • 更简单的基于文件的应用接口
  • 完全支持消息优先级(优先级决定队列中消息的位置)
  • 完全支持消息到达的异步通知,这是通过信号或者线程创建实现
  • 用于阻塞发送与接受的超时机制

留下评论