进程通信(1)

在多道程序系统中,为了协调进程之间的工作,并发进程经常需要交换信息,尤其是一个作业中的多个进程之间,为了合作完成一项任务,有时还要交换大批数据。进程之间的信息交换称为进程间通信(InterProcess Communication,IPC)。

进程之间只需要交换少量的控制信号(如信号量)就可以实现彼此的同步,但是,为了同步而交换的控制信息并不是进程通信的目的,最终的目标是,在条件合适时,交换大量的纯数据,它不仅是为了控制双方的推进速度(P/V原语),而且为了让对方能接受到数据,进而对数据进行处理。

根据进程之间交互信息的类型,可把进程通信分为低级通信和高级通信两类。所谓低级通信是指进程之间交换少量的控制信息,以实现进程的同步和互斥;而高级通信是进程间交换大量的数据,以实现数据的处理。低级通信交换的是控制信息,信息量少、效率低;高级通信交换的是普通数据,数据量大、通信效率高,这里主要介绍高级通信。

1、最早的IPC方法:信号与通道

信号是用整数表示的系统异步事件,信号最初用于表示硬件错误(浮点溢出、访问越界),系统内核将其转换为信号,通知相应的程序做出相应的处理。它是系统中进程间的一种低级通信形式。

Linux系统中所支持的信号类型与具体的平台相关,由于内核中用一个字代表所有的信号,每个位对应一种信号,所以信号类型的最大数目取决于机器的长度。例如32位字长的处理机最多支持32种信号,部分信号如下:

宏名 用途 宏名 用途
SIGHUP 从中断发出的结束信号 SIGKILL 结束接受信号的进程
SIGINT 来自键盘的中断信号Ctrl+C SIGALRM 定时信号
SIGQUIT 来自键盘的退出信号Ctrl+\ SIGTERM kill命令发出的信号
SIGFPE 浮点异常信号 SIGCHLD 标志子进程结束或停止的信号

信号机制的最大特点就是异步性,一个进程可以在任意时刻接受信号,但是只有当进程进入运行状态时才能对接受信号做出响应,这种现象与中断类似,为了区别于硬件中断,信号又称“软中断”。只有内核和超级用户可以向其他进程发送信号,普通进程只能向具有相同Uid和Gid的进程或同一进程组的其他进程发送信号。
信号的发送通过设置进程task_struct结构中的signal域中的某一位产生的,每次进程从系统调用中退出时,都会检查signal和blocked域,检查是否有立刻可以发送的信号。
Linux系统中,信号之间无优先关系,同类信号也无特定原则。当多个信号同时发送时,进程可以以任意顺序接收到,也可以按任意次序处理。在进程的task_struct中有一个指针指向sigaction数组,数组的每一个元素记录了进程待处理的信号及处理每个信号的程序的入口地址。信号的处理有很多种,完全由接受者来决定,可归纳为四类:

1.忽略信号:进程可忽略除某些信号之外的所有信号。首先检查sigaction数组,如果信号不是SIGKILL和SIGSTOP且被忽略时,则不对该信号做任何处理;
2.阻塞信号:进程可以阻塞某些信号;
3.由进程处理该信号:进程可以在系统中注册处理信号的处理程序地址,接受该信号时由注册的处理程序处理信号;
4.由内核进行默认处理:大多数情况,信号由内核处理。

Linux中有关信号的信号调用如下所示:

调用原型 功能
int signal(int signum,void(*handler)(int)) 发送信号
int sigaction(sig,&handler,&oldhandler) 定义与信号的处理
int sigprocmask(int how,sigset_t *mask,sogset_t *old) 检查或修改信号屏蔽
int pause(void) 挂起进程

管道通过共享文件方式实现大批数据的传送,是linux系统中的一种高级通信方式。实际上,Linux系统中的管道是由文件系统的高速缓冲区实现的,其容量被限定在4KB。进程通过管道传送信息时,首先建立一个管道文件将写进程与读进程联系起来,此后,写进程可将信息写入管道文件中,而读进程可以从管道文件中读出信息。
这种通信本质上是一个生产者/消费者方式工作的环形缓冲区问题,但其部分同步和互斥工作已由操作系统完成。例如当缓冲区已经满时,操作系统会自动阻塞写进程write(),直至数据被取走;读进程每次从管道中读取数据,系统就会将这部分数据删除。当管道为空时,系统会阻塞读进程read()。
在Linux中,大多数shell命令支持管道操作,它可以将一个进程的标准输出送到下一个进程的标准输入中,实现多个进程间的单向传送,形式如下:

命令1|命令2|...|命令n

在Linux中,管道将两个文件描述符指向同一临时的虚拟文件系统的inode,而该VFS inode指向内存中的一个物理页面。其中,一个file结构实现对管道文件的写操作,而另一个实现对文件的读操作,页面就是读/写进程共享的缓冲区。写进程将数据复制到页面的尾部;读进程从页面的头部复制数据。

Linux下管道分两种:无名管道和命名管道。无名管道就是一般意义上的管道,通过系统调用pipe建立临时文件,物理上由高速缓冲区构成,很少启用外设。进程通信完成后,系统回收文件的索引节点。无名管道只能有管道的创建者及其子进程使用。命名管道,即FIFO管道,是一个按名存取的文件,可以在文件系统中长期存在,任一进程都可按通常的文件存储方法存取命名管道。无名管道的操作如下:
1.创建管道——pipe():原型int pipe(int fd[2]),创建无名管道,如果系统调用成功,返回0;如果失败返回-1。管道创建后,管道两端可分别用描述字fd[0]以及fd[1]来描述,其中,fd[0]只用于读,称为管道读端;fd[1]只用于写,称为管道写端。
2.向管道写入数据——write(fd[1],buf,size):写入数据时,先检测管道是否有足够空间?内存是否被读进程锁住?如果都满足则增加管道的大小,写入所有数据。
3.从管道中读出数据——read(fd[0],buf,size)读取数据。
4.关闭管道的一端——close(fd[i])

简单举例:子进程向管道中写入数据,父进程从管道中读取数据。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(void)
{
  int fd[2],nbytes;
  pid_t child;
  char buf[80];

  pipe(fd);/*建立管道*/
  if((child=fork())==-1)
    {
    printf("Error in forking child!");
    exit(-1);
    }
  if(child == 0)
    {//子进程创建成功,写进程
    close(fd[0]);//关闭读端
    printf("Child is writing\n");
    sprintf(buf,"This is child!\n");
    write(bf[1],buf,30);
    close(fd[1]);
    exit(0); 
    }
  else
    {
    close(fd[1]);//关闭写端
    nbytes=read(fd[0],buf,sizeof(buf));
    printf("Father has read the data!\n");
    close(fd[0]);  
    /*收集子进程退出信息*/  
    waitpid(child,NULL,0);  
    }
  return(0);
}

无名管道的特点是简单、高效,但也存在一些缺陷:只支持单向数据流,不支持全双工通信;家族关系的进程之间;缓冲区大小有限;传送的是无格式字节流,发收双方要事先约定规则;管道中信息不能长久保存。

命名管道(FIFO管道),是在无名管道之上,克服无名管道的缺陷而提出的一种按名存取的文件,可在文件系统中长期存在,任何进程都可以按文件存取方法操作管道。系统调用:
1.创建命名管道
int mkfifo(const char *pathname,mode_t mode);
int mknod(char *pathname,mode_t mode,dev_t dev);
第一个参数是FIFO的名称,第二个mode与文件open()函数的参数mode相同。
2.打开管道open,操作前需要先打开管道。
3.管道的读/写,与无名管道相同。

/*fifo_write.c*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FIFO_SERVER "/tmp/myfifo"

main(int argc,char** argv)
{
    int fd;
    char w_buf[100];
    int nwrite;
    /*打开有名管道,并设置为非阻塞*/
    fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
    if(argc==1)
        printf("Please send something\n");
    strcpy(w_buf,argv[1]);
    /*向管道写入字符串*/
    if((nwrite=write(fd,w_buf,100))==-1)
    {
        if(errno==EAGAIN)
            printf("The FIFO has not been read yet.Please try later\n");
    }
    else 
        printf("write %s to the FIFO\n",w_buf);
}


/*fifo_read.c*/

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FIFO "/tmp/myfifo"

main(int argc,char** argv)
{
  char buf_r[100];
  int  fd;
  int  nread;
  
  /*创建有名管道,并设置相应的权限*/
  if((mkfifo(FIFO,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
    printf("cannot create fifoserver\n");
  printf("Preparing for reading bytes...\n");
  
  memset(buf_r,0,sizeof(buf_r));
  /*打开有名管道,并设置非阻塞标志*/
  fd=open(FIFO,O_RDONLY|O_NONBLOCK,0);
  if(fd==-1)
  {
    perror("open");
    exit(-1);  
  }
  while(1)
  {
    memset(buf_r,0,sizeof(buf_r));
    /*读取管道中的字符串*/
    if((nread=read(fd,buf_r,100))==-1){
      if(errno==EAGAIN)
        printf("no data yet\n");
    }
    printf("read %s from FIFO\n",buf_r);
    sleep(1);
  }  
  pause();
  unlink(FIFO);
}

System V的通信机制见下一篇文章。

计算机系统的硬件结构主要由四部分组成:控制器、运算器、内存和输入输出设备,其中,控制器和运算器统称为中央处理器。简称CPU,它是计算机硬件系统的指挥中心。其中,控制器的功能是

控制计算机各部分协调工作

,运算器则是负责计算机的算术运算和逻辑运算。运算器包括:算术逻辑运算单元ALU、浮点运算单元FPU、通用寄存器组和专用寄存器;控制器包括:指令控制器、时序控制器、总线控制器和中断控制器。
posted @ 2014-01-12 10:11  侯凯  阅读(654)  评论(0编辑  收藏  举报