最近想为系统添加一个统计脚本,但是系统内的模块是有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\n";
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执行,补上结束标志。