linux下fifo,mq,shm

最近想为系统添加一个统计脚本,但是系统内的模块是有perl和java两种语言编写,且模块是通过crontab定时调用的,所以需要使用IPC传输信息。

第一个想到的是socket方式,感觉需要统一设定一个端口,不是特别保险,并且全都在一台服务器上,用socket有些浪费,所以打算尝试一下linux的IPC功能。

1.FIFO. 接着想起FIFO,即有名管道。在linux中是一种特殊的文件,因为linux中所有事物都可以被视为文件,所以对FIFO的操作和对文件的操作方式是一样的。

可以使用mkfifo命令创建一个有名管道:

$ mkfifo fifo
$ ll fifo
prw-r--r-- 1 star star 0 Feb 14 15:42 fifo|

属性‘p’表示这个文件是个管道,最后的‘|’也是相同的意义。

先上perl代码:

读管道:
my $fifo = 'fifo';
# 和普通文件一样打开
open( my $fh, "<", $fifo) or die "open($fifo) failed: $!";
seek($fh,0,0);
while(1)
{
  while(my $line = readline($fh))
  {
    print "$line";
  }
}

。。。

写管道:
 my $fifo = 'fifo';
 open( my $fh, ">", $fifo) or die "open($fifo) failed: $!";print $fh "test fifo
";

FIFO是一种先进先出的文件,没有seek等的操作,只能从开始读,写到末尾。与普通文件不同之处在于:

a) 如果当前是为读打开FIFO,则需要有其他进程为写打开FIFO,否则会阻塞(没有设置非阻塞标志)。

b) 如果是为写打开,则需要有其他进程为读打开。

也就是说,默认情况下,读和写至少都要有一个才能正常打开FIFO,如果想要不阻塞在打开操作上,可以使用O_NONBLOCK标志。参考自:http://blog.csdn.net/xiaobai1593/article/details/7402205

sysopen( my $fh, $fifo, O_WRONLY | O_NONBLOCK) or die "open($fifo) failed: $!";

后来没有使用FIFO,原因有两个:

a) java端虽然可以正常操作FIFO文件,但是java的打开文件操作没有O_NONBLOCK方式,如果另一方进程由于非正常原因退出了,那么java会一直阻塞在打开操作上。如果通过设置超时等方式处理,会比较繁琐。

b) 多次写入后,一次读取有可能读取不完整。

另外值得注意的是,对FIFO而言,当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。 PIPE_BUF这个值是可以设置的,所以如果一次写入的数据很小,可以不用担心写写冲突的问题。如果是读的话,应该是只有一个读者才好。

2. shared memory

共享内存在linuxIPC中是性能最高的,因为别的方式需要先把数据复制到内核区,再复制到用户区,而共享内存全部在内存,不经过内核区。

并且,共享内存是可以设置IPC_PRIVATE方式的,提高独立性。

但是它的操作方式和文件类似,这就会有内容覆盖和需要制定位置的问题。也就是说,每次写入新的内容,很容易出错就覆盖掉老的内容,读的时候还得把读过内容删除掉,非常麻烦。

所以没有选择共享内存的方式。


3. Message Queue

最后比较下来,选用了消息队列的方式。

消息队列的好处:

1. 每个消息是相互独立的,数据是被包装成条消息,没有上面的内容覆盖问题。读取都是对消息的操作,比较方便。

2. 操作系统层面上会保证每个消息的完整,不会有写写冲突的存在。

消息队列需要注意的是:读写之间的配合,如果写入过多会导致消息队列到达容量上限,不能再写入;写入的数据不能过大,可以在 /etc/sysctl.conf中设置。http://www.cnblogs.com/samurail/archive/2012/11/14/2770379.html

perl可以直接创建一个消息队列,这点是java做不到的,java只能打开一个已经存在的消息队列。

所以如果想要perl和java之间通过消息队列交互,需要先用perl创建一个消息队列,再把这个消息队列的编号保存在一个文件中,java读取文件获得编号,打开mq进行操作。

perl创建代码如下(使用的是SysV标准):

use IPC::SysV qw(IPC_PRIVATE IPC_RMID IPC_CREAT S_IRUSR S_IWUSR IPC_NOWAIT);
use IPC::Msg;

$key = IPC::SysV::ftok($file,'a');
$msg =new IPC::Msg($key,0666|IPC_CREAT) or logAbort("create message queue:$!");

移除:

$msg->remove();

为了确保程序意外终止的情况下,不会有消息队列遗留,remove操作最好放在END{}代码区内。

读取:

# infinite loop to read messages from MQ
while (1) {
  $msg->rcv($buf,1024,$msgtype);
  if ($!) {
    killThread($thr);
    logAbort("read message queue failed: $!");
  }
  $q->enqueue($buf);
}

$msg->rcv在没有IPC_NOWAIT时会阻塞至有数据来,所以会新建一个线程专门读取mq数据。

写入:

$msg->snd($msgtype, $buf, IPC_NOWAIT) or logWarning("writing message to mq failed. $!");

其中读和写的$msgtype需要一致才行,最简单的就是都设为1。这里设置了IPC_NOWAIT,因为写操作不能阻塞,会影响正常程序运行。

http://perldoc.perl.org/perlipc.html#SysV-IPC 

java要操作消息队列有点繁琐,因为java本意是为了跨平台运行,而消息队列是操作系统相关的,所以如果想要操作mq,需要使用JNI。

需要使用的包是lajp-10.05,这是专门开发用来结合java和php优点的包,但是在目前,我们只需要使用其中的IPC部分。

另外,java的项目管理通常都是使用maven,而lajp并不在maven的几个主流仓库中,所以,要想在maven中纳入lajp包,需要做一些额外的工作,

比较方便的是使用maven把lajp包放入本地的maven仓库,这样可以随时使用:

mvn install:install-file -Dfile=./lajp-10.05/lajp-10.05.jar -DgroupId=lajp -DartifactId=lajp -Dversion=10.5 -Dpackaging=jar -DgeneratePom=true

成功之后,在pom里就可以和往常一样把lajp写入<dependency></dependency>中。在java代码中还得加入:

static {
        //JNI
        System.loadLibrary("lajpmsgq");
        });
    }

java操作mq:

import lajp.MsgQ;

Integer key;
// 打开
msqid = MsgQ.msgget(key);
//
// 1:和perl中$msgType一致 MsgQ.msgsnd(msqid, 1, str.getBytes(), str.getBytes().length);

这里的key即时前文中perl写在文件中的key。

4. 命令行查看和删除IPC

查看命令:

ipcs

删除命令:

ipcrm

可以指定删除某一个IPC对象

usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
          [-Q msgkey] [-M shmkey] [-S semkey] ... ]

ps:perl中,在使用了chdir之后,再使用abs_path,结果和预料的有时候会有偏差。。。

在操作IPC对象时,需要考虑如果进程突然终止或退出了的情况,这时候往往会在IPC对象中留下遗留数据(比如在统计时间时,需要写入结束标志才能统计一个任务的时间,而进程突然中止后,mq中收不到结束标志了,另一个统计进程会认为这个任务一直在运行)。这时候需要使用一些预防措施,如在java中使用

Runtime.getRuntime().addShutdownHook()

函数,在退出时,会由jvm执行,补上结束标志。

原文地址:https://www.cnblogs.com/starRebel/p/IPC.html