Linux下进程通信

Linux下进程通信
2011年05月07日
  Linux下进程通信的八种方法[连载-记1]:所有方法登场 本连载是我对《Linux Programming by Example》《Linux Aplication Development》《Linux应用开发技术详解》等书中介绍的Linux下进程通信的方法的相关章节作的笔记,具体细节的还请参照以上书籍。本连载是我对《Linux应用开发技术详解》一书中介绍的Linux下进程通信的方法的相关章节作的笔记。
  Linux下进程通信的八种方法:管道(pipe),命名管道(FIFO),内存映射(mapped memeory),消息队列(message queue),共享内存(shared memory),信号量(semaphore),信号(signal),套接字(Socket).
  (1) 管道(pipe):管道允许一个进程和另一个与它有共同祖先的进程之间进行通信;
  (2) 命名管道(FIFO):类似于管道,但是它可以用于任何两个进程之间的通信,命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建;
  (3) 信号(signal):信号是比较复杂的通信方式,用于通知接收进程有某种事情发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持UNIX早期信号语义函数signal外,还支持语义符合POSIX.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD即能实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数的功能);
  (4) 内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它;
  (5) 消息队列(message queue):消息队列是消息的连接表,包括POSIX消息对和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能成该无格式字节流以及缓冲区大小受限等缺点;
  (6) 信号量(semaphore):信号量主要作为进程间以及同进程不同线程之间的同步手段;
  (7) 共享内存 (shared memory):它使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。这是针对其他通信机制运行效率较低而设计的。它往往与其他通信机制,如信号量结合使用,以达到进程间的同步及互斥;
  (8) 套接字(Socket):它是更为通用的进程间通信机制,可用于不同机器之间的进程间通信。起初是由UNIX系统的BSD分支开发出来的,但现在一般可以移植到其他类UNIX系统上:Linux和System V的变种都支持套接字;
  linux进程间的同步方法
  http://buaadallas.blog.51cto.com/399160/171061
  进程间通讯(IPC)方法主要有以下几种:
  管道/FIFO/共享内存/消息队列/信号
  1.管道中还有命名管道和非命名管道(即匿名管道)之分,非命名管道(即匿名管道)只能用于父子进程通讯,命名管道可用于非父子进程,命名管道就是FIFO,管道是先进先出的通讯方式
  2.消息队列是用于两个进程之间的通讯,首先在一个进程中创建一个消息队列,然后再往消息队列中写数据,而另一个进程则从那个消息队列中取数据。需要注意的是,消息队列是用创建文件的方式建立的,如果一个进程向某个消息队列中写入了数据之后,另一个进程并没有取出数据,即使向消息队列中写数据的进程已经结束,保存在消息队列中的数据并没有消失,也就是说下次再从这个消息队列读数据的时候,就是上次的数据!!!!
  3.信号量,它与WINDOWS下的信号量是一样的,所以就不用多说了
  4.共享内存,类似于WINDOWS下的DLL中的共享变量,但LINUX下的共享内存区不需要像DLL这样的东西,只要首先创建一个共享内存区,其它进程按照一定的步骤就能访问到这个共享内存区中的数据,当然可读可写
  以上几种方式的比较:
  1.管道:速度慢,容量有限,只有父子进程能通讯
  2.FIFO:任何进程间都能通讯,但速度慢
  3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
  4.信号量:不能传递复杂消息,只能用来同步
  5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
  进程通信---FIFO
  http://blogold.chinaunix.net/u3/102839/showart_204 1236.html
  管道没有名字,所以只能在具有血缘关系的进程间使用,而在无名管道发展出来的有名管道FIFO,则有路径名与之相关联,以一种特殊设备文件形式存在于文件系统中,从而允许无亲缘关系的进程访问FIFO,下面看FIFO的详细操作
  1.FIFO的建立
  FIFO是存在于文件系统的文件节点,所以我们可以建立文件节点的mknod系统用来建立它,也可以mkfifo系统调用
  mkfifo说明:
  #include
  #include
  int mkfifo(char *path,mode_t mode);
  说明:path:路径名,mode:指定文件存取权标志,mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,系统调用已经指定O_CREATE|O_EXCL
  返回:若成功则返回0,否则返回-1,错误原因存于errno中。
  错误代码
  EACCESS 参数pathname所指定的目录路径无可执行的权限
  EEXIST 参数pathname所指定的文件已存在。
  ENAMETOOLONG 参数pathname的路径名称太长。
  ENOENT 参数pathname包含的目录不存在
  ENOSPC 文件系统的剩余空间不足
  ENOTDIR 参数pathname路径中的目录存在但却非真正的目录。
  EROFS 参数pathname指定的文件存在于只读文件系统内。
  2.FIFO使用
  创建后,在读写前,要先打开它,用open系统调用
  当使用open()来打开 FIFO文件时,O_NONBLOCK旗标会有影响
  1、当使用O_NONBLOCK 旗标时,打开FIFO 文件来读取的操作会立刻返回,但是若还没有其他进程打开FIFO 文件来读取,则写入的操作会返回ENXIO 错误代码。
  2、没有使用O_NONBLOCK 旗标时,打开FIFO 来读取的操作会等到其他进程打开FIFO文件来写入才正常返回。同样地,打开FIFO文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回。
  下面练习,分别写两个程序,一个是服务器程序,不断从管道读取客户发送的信息;另一个是客户程序,在命令行输入信息并从管道发送:
  客户程序(写管道)
  /*fifo_write.c*/
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  /*FIFO管道路径*/
  #define FIFO_SERVER "/tmp/myfifo"
  main(int argc,char** argv)
  {
  int fd = 0;
  char w_buf[100];
  int nwrite;
  /*打开FIFO管道*/
  fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
  if(fd==-1)
  if(errno==ENXIO)
  printf("open error; no reading process\n");
  /*判断有没有参数输入*/
  if(argc==1)
  printf("Please send something\n");
  /*复制参数输入*/
  strcpy(w_buf,argv[1]);
  /*写到FIFO去*/
  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
  #include
  #include
  #include
  #include
  #include
  #include
  /*定义FIFO路径*/
  #define FIFO "/tmp/myfifo"
  main(int argc,char** argv)
  {
  char buf_r[100];
  int fd;
  int nread;
  /*创建FIFO管道*/
  if((mkfifo(FIFO,O_CREAT|O_EXCL))&&(errno!=EEXIST))
  printf("cannot create fifoserver\n");
  printf("Preparing for reading bytes...\n");
  memset(buf_r,0,sizeof(buf_r));
  /*打开FIFO管道,不阻塞方式*/
  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);
  }
  接下来进行编译,编译好后,在Linux中运行两个终端,分别运行以上两个程序,可以看到,运行fifo_read时,程序一直在每隔一秒读,然后我们在另一个终端输入:
  $ ./fifo_write helloworld
  可以看出fifo_read显示出"helloworld",说明接受成功
  FIFO的一些注意问题:
  (1)管道数据的FIFO处理方式
  首先放入管道的数据,在端口首先被读出
  (2)管道数据的不可再现性
  已读取的数据在管道里消失,不能再读
  (3)管道长度的有限性
  (4)SIGPIPE信号 如果一个进程试图写入到一个没有读取到进程的管道中哦你,系统内核产生SIGPIPE信号
  【转帖】信号量与自旋锁 |Linux,信号量,自旋锁,睡眠锁,spinlock,semaphore
  2008-11-18 15:14内核同步措施
  为了避免并发,防止竞争。内核提供了一组同步方法来提供对共享数据的保护。 我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别。
  Linux 使用的同步机制可以说从2.0到2.6以来不断发展完善。从最初的原子操作,到后来的信号量,从大内核锁到今天的自旋锁。这些同步机制的发展伴随 Linux从单处理器到对称多处理器的过度;伴随着从非抢占内核到抢占内核的过度。锁机制越来越有效,也越来越复杂。
  目前来说内核中原子操作多用来做计数使用,其它情况最常用的是两种锁以及它们的变种:一个是自旋锁,另一个是信号量。我们下面就来着重介绍一下这两种锁机制。
  自旋锁
  -------------------------------------------------- ----
  自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
  自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环--旋转--等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
  事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
  自旋锁的基本形式如下:
  spin_lock(&mr_lock);
  //临界区
  spin_unlock(&mr_lock);
  因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。在单处 理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。
  简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁--因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。
  死锁:假设有一个或多个内核任务和一个或多个资源,每个内核都在等待其中的一个资源,但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待, 但它们永远不会释放已经占有的资源,于是任何内核任务都无法获得所需要的资源,无法继续运行,这便意味着死锁发生了。自死琐是说自己占有了某个资源,然后 自己又申请自己已占有的资源,显然不可能再获得该资源,因此就自缚手脚了。
  信号量
  -------------------------------------------------- ----
  Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行 其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
  信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。
  信号量基本使用形式为:
  static DECLARE_MUTEX(mr_sem);//声明互斥信号量
  if(down_interruptible(&mr_sem))
  //可被中断的睡眠,当信号来到,睡眠的任务被唤醒
  //临界区
  up(&mr_sem);
  信号量和自旋锁区别
  -------------------------------------------------- ----
  虽然听起来两者之间的使用条件复杂,其实在实际使用中信号量和自旋锁并不易混淆。注意以下原则:
  如果代码需要睡眠--这往往是发生在和用户空间同步时--使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更加简单一些。如果需要在自旋 锁和信号量中作选择,应该取决于锁被持有的时间长短。理想情况是所有的锁都应该尽可能短的被持有,但是如果锁的持有时间较长的话,使用信号量是更好的选 择。另外,信号量不同于自旋锁,它不会关闭内核抢占,所以持有信号量的代码可以被抢占。这意味者信号量不会对影响调度反应时间带来负面影响。
  自旋锁对信号量
  -------------------------------------------------- ----
  需求 建议的加锁方法
  低开销加锁 优先使用自旋锁
  短期锁定 优先使用自旋锁
  长期加锁 优先使用信号量
  中断上下文中加锁 使用自旋锁
  持有锁是需要睡眠、调度 使用信号量
  以下部分的来源:kcrazy的纸¨
  自旋锁我的理解就好比
  小A,小B,小C,小D 同住一个屋子,可屋子只有一间茅房和一个马桶。他们谁想"便"的时候谁就要把茅房的门锁上,然后占据马桶,比如小A正在占有,聚精会神,非常惬意。碰巧小 C此时甚急,但没办法,因为小A已经把门上了锁。于是小B在门口急得打转,即为"自旋"。注意这个"自旋"两个字用的好,小B不是一看门被上锁就回屋睡觉 去了,而是在门口"自旋"。... 最终的结果是小A开锁,小B占用。而且在开锁闭锁过程中动作干净利落,不容他人抢在前面。
  如此周而复始......
  这里的 小A,B,C,D 即为处理器,茅房的锁即为自旋锁。当其他处理器想访问这个公共的资源的时候就要先获取这个锁。如果锁被占用,则自旋(循环)等待。
  小A的聚精会神代表了IRQL为2,开关锁动作快表示为原子操作。
  -------------------------------------------------- --------
  不知道我理解的对还是不对,可能这样举例有些不恰当。有理解不对之处希望指点一二,以免误入歧途,悔之晚矣。
  -------------------------------------------------- --------
  写了个测试程序测试了一下:
  KSPIN_LOCK spinlock;
  NTSTATUS DriverEntry(
  IN PDRIVER_OBJECT DriverObject,
  IN PUNICODE_STRING RegistryPath
  )
  {
  NTSTATUS Status;
  UNICODE_STRING DeviceName;
  PDEVICE_OBJECT DeviceObject;
  HANDLE ThreadHandle;
  KIRQL oldirql;
  KIRQL irql;
  ULONG Processor;
  ULONG i;
  DeviceObject = NULL;
  RtlInitUnicodeString( &DeviceName, deviceNameBuffer );
  Status = IoCreateDevice( DriverObject,
  0,
  &DeviceName,
  FILE_DEVICE_UNKNOWN,
  0,
  FALSE,
  &DeviceObject );
  if ( !NT_SUCCESS(Status) )
  {
  return Status;
  }
  DriverObject->DriverUnload = DriverUnload;
  KeInitializeSpinLock( &spinlock ); // (2)
  PsCreateSystemThread( &ThreadHandle, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadRoutine, NULL );
  i = 10000;
  KeAcquireSpinLock( &spinlock, &oldirql );
  while (i--)
  {
  __asm nop
  irql = KeGetCurrentIrql();
  Processor = KeGetCurrentProcessorNumber();
  KdPrint(( " [%d] CurrentIrql:\t%d", Processor, irql ));
  }
  KeReleaseSpinLock( &spinlock, oldirql ); return Status;
  } VOID
  ThreadRoutine( IN PVOID StartContext )
  {
  KIRQL oldirql;
  KIRQL irql;
  ULONG Processor;
  ULONG i; i = 10000; KeAcquireSpinLock( &spinlock, &oldirql ); // (1) while (i--)
  {
  __asm nop
  irql = KeGetCurrentIrql();
  Processor = KeGetCurrentProcessorNumber(); KdPrint(( "**[%d] CurrentIrql:\t%d", Processor, irql ));
  } KeReleaseSpinLock( &spinlock, oldirql ); // (1)
  } -------------------------------------------------- ------------------------------- 首先说明一下我是双核系统,如果是单核的话我想进入自旋锁之后IRQL已经提高到 DPC 级别,第二个线程就跑不起来了。如果他神奇的跑了起来,那一定会发生死锁。
  分几种情况测试:
  1、就是上边的代码测试
  先抓到锁的先跑,后抓到锁的后跑。并且被锁的期间的IRQL 为 DPC 级别。
  2、去掉 标记 (1) 的两行
  结果是两个线程同时跑,一个占处理器 [0] 一个占处理器 [1]
  上锁的那个 IRQL 级别是 DPC 级。
  没上锁的IRQL为 0
  即 自旋锁 并不影响其他处理器的正常运行。除非其他处理器也想获得这个锁。
  3、去掉 标记 (2) 的一行(spinlock 是全局变量)
  和 1 的结果相同,因为全局变量默认是初始化为0的。

你可能感兴趣的:(操作系统)