Shell Programlama: iki script arasında "named pipe" ile haberleşme

taydin

Timur Aydın
Yönetici
Shell (kabuk) programlama denince artık akla büyük ölçüde BASH için yazılmış script'ler gelir. Bu konuda, bir sunucu gibi çalışan ve hafızada sürekli olarak duran bir "sunucu" script'i ile buna komut gönderen bir "istemci" script nasıl yapabileceğimize bakacağız. Burada sunucu sürekli olarak komut bekliyor olacak ve bir komut alınca, bunun gereğini yapacak ve tekrar komut beklemeye başlayacak. Birden fazla istemci aynı anda komut gönderebiliyor olacak ve sunucu bunların ikisini de alıp gereğini yapabiliyor olacak.

Böyle bir sunucuyu shell programlama ile genel anlamda aşağıdaki metotlarla yapabiliriz:

1) Sunucunun, belli bir dizini gözlemesini, ve bu dizin içerisine bir dosya oluşturulursa, bu dosyayı okuyup içindeki komutu işleyebilir. Komut işlendikten sonra da dosya silinebilir. Bu yöntem, basit gibi görünse de, beklenmedik zorlukları vardır. Örneğin istemcilerin, kesinlikle farklı isimli bir dosya kullanmaları lazım, yoksa bir istemci, diğer istemcinin komut dosyasının üzerine yazabilir. İkinci zorluk, yeni bir dosya oluştuğunun algılanmasıdır. Burada genellikle "inotify" mekanizması kullanılır. Ama inotify kullanıldğında da, sadece bu program aktif iken değişiklikler rapor edilir. Bir değişiklik algılanıp gereği yapılmak üzere inotify çıktığında, artık yeni bir dosya oluştuğunu farkedemeyiz. Dizini tarayabiliriz, ama tam biz taramayı yaptıktan sonra yeni bir komut dosyası gelebilir. Bu zorluklar nedeniyle, bu yöntemi elememiz lazım.

2) Sunucuya "signal" göndermek. Mesela X komutu için SIGUSR1, Y komutu için SIGUSR2, Z komutu için SIGHUP kullanılabilir. Ama bu yöntemin de en büyük sıkıntısı, hem gönderilecek komutlar kısıtlı, hem komutlara parametre veremiyoruz, hem de bir komut bitmeden yeni komut gönderemiyoruz. O yüzden bu metot da hemen elenebilir.

3) Belli bir mekanizma ile bir komutu alıp onu stdout'a yazan özel bir program kullanmak. Bu metot çok güzel işe yarar tabiki. Mesela socket'ler üzerinden komut alan bu program, bir döngü içerisinde sürekli olarak script'e komutları besleyebilir. Ama böyle özel bir programı shell dışında başka bir dil ile yazdıysak, o zaman shell ile niye uğraşıyoruz? direkt bütün işi o programlama dilinde bitirelim :) O yüzden bu metot da eleniyor.

4) "named pipe" kullanmak. İşte bu metot, tamamen shell script dahilinde bize çok güzel bir çözüm sunuyor. Ama bunu doğru bir şekilde çalışır hale getirmek için birçok detay vardır.

İşte bu konuda 4 numaralı çözümü aşama aşama anlatacağız. Önce en basit bir şekilde gerçekleştireceğiz, ortaya çıkan problemleri gözlemleyeceğiz ve bu problemleri ortadan kaldıran daha gelişmiş çözüme geçeceğiz.
 

taydin

Timur Aydın
Yönetici
sunucu ve istemci programının ilk versiyonunu hazırlıyoruz

SUNUCU
Kod:
#!/bin/bash

CMDFIFO=/tmp/fifo

if [ ! -p $CMDFIFO ]; then
    mkfifo $CMDFIFO
fi

while read LINE < $CMDFIFO; do
    echo "LINE = $LINE"
done

İSTEMCİ
Kod:
#!/bin/bash

CMDFIFO=/tmp/fifo

if [ -p "$CMDFIFO" ]; then
    echo "deneme" > $CMDFIFO
fi
 

taydin

Timur Aydın
Yönetici
İki tane terminal açıyoruz, birisinde sunucuyu çalıştırıyoruz ve diğerinde de istemciyi. Hakikaten gönderdiğimiz komut, sunucu tarafından görüntüleniyor. Yeni komut gönderdiğimizde de bu komut da görüntüleniyor.

Screenshot_20190227_111434.png
 

taydin

Timur Aydın
Yönetici
İş halloldu gibi duruyor değil mi? :) Ama gerçekte, iki tane ciddi problem var ve bu problemler de ancak uygun şartlarda ortaya çıkıyor. Problemlerden birisi, komutlar hızlıca gönderildiğinde ortaya çıkıyor. Maksimum hızda komut göndermek için, istemcinin içindeki komutu direkt olarak sonsuz döngü şeklinde gönderiyoruz:

Kod:
$ while true; do echo "deneme" > /tmp/fifo; done

Bunu yaptığımızda, sunucu belli sayıda komutu alıp görüntülüyor. Ama sonrasında komut gönderilen terminal aniden kapanıp gidiyor ve sunucu script'i de sonlanıyor. istemci tarafını strace altında çalıştırınca SIGPIPE signal geldiği için istemci sonlandırılmış oluyor. Evet, çok hızlı komut gönderiminde sunucu çalışmıyor.
 
Son düzenleme:

taydin

Timur Aydın
Yönetici
Burada iki problem var. Birincisi, her seferinde "read" komutunun named pipe'ı açması, okuması ve sonrasında kapatıp tekrar açması. Pipe'in bir tarafı kapatıldığında, pipe'ın diğer ucundaki program işte böyle SIGPIPE ile sonlandırılıyor. Bu signal'ı yakalayıp programın sonlanmasını engelleyebiliriz, ama o named pipe artık kullanılamaz duruma gelmiş oluyor ve program sonlanmasa da takılıp kalacaktır. O yüzden yapmamız gereken, sunucu tarafında her seferinde "read" ile named pipe'ı açıp kapatmadan, bir kere açıp onun üzerinden devam etmek. Bunu yapmanın da yolu, her komutu tek bir satıra yerleştirmek ve böylece bu komutların tamamının aynı alt kabuk (subshell) içinde işlenmesini sağlamak.

Buradaki ikinci sorun da, istemci tarafında komutu yazdıktan sonra istemci sonlanıyor. İşte bu sonlanma anında bu sefer named pipe'in sunucu tarafı sürekli olarak "end of file" veya EOF almaya başlıyor ve kullanılamaz hale geliyor, çünkü pipe'in bir ucu artık boşta. Bunu engellemek için de, sunucu tarafında pipe'ın diğer ucuna bir sabit istemci koyuyoruz. Bu istemcinin tek amacı, named pipe'ın diğer ucunun boşa çıkmasını engellemek. Aşağıda bu iki değişikliğin yapıldığı script'i görüyoruz


Kod:
#!/bin/bash

CMDFIFO=/tmp/fifo

# create a permanent client for the pipe to prevent EOF
exec 3<>$CMDFIFO

if [ ! -p $CMDFIFO ]; then
    mkfifo $CMDFIFO
fi

# put all commands on a single line to make them run under the same subshell
while true; do read LINE; echo "LINE = $LINE"; done < $CMDFIFO
 
Son düzenleme:

taydin

Timur Aydın
Yönetici
Bu iki düzeltmeyi yaptıktan sonra sunucuyu çalıştırıyoruz. istemci tarafından da maksimum hızda komut gönderiyoruz ve komutların herhangi bir sorun çıkmadan sunucuda işlendiğini görüyoruz. Birden fazla istemciye aynı anda komut göndertiyoruz ve komutların dönüşümlü olarak sunucuya ulaştığını görüyoruz.
 

theroot

Kayıtlı Üye
iki script arasında soket ile haberleşme için "nc" programı da kullanılabilir. Her iki script de aynı host üzerinde çalışıyorsa, örneğin bir UDP soket üzerinden kayıpsız veri alışverişi elde edilebilir. Ama kesinlikle pipe kullanmaktan daha zor olacaktır ve hem UDP protokolü ile ilgili, hem de nc'nin kullanımı ile ilgili ayrıntılara iyi hakim olmak gerekir.
 

taydin

Timur Aydın
Yönetici
Eğer script'in yaptığı iş böyle özel bir IPC programının kullanımını gerekli kılıyorsa, o zaman bu işin shell script ile yapılmasını sorgulamamız gerekir. Böyle durumlarda en iyisi, o programı örneğin python veya perl gibi bir dilde yazmak. Bu tabiki programı bash script ile yazmaktan daha zor olacak, ama aynı programı C/C++ ile yazmaktan çok daha kolay olacak.
 
Üst