W Linuksie można programować moduły w natywnym dla niego języku czyli C, co okazuje się dla niektórych zbyt dużym ograniczeniem/niewygodą. C jest językiem dosyć niskopoziomowym ze wszystkimi wadami i zaletami tej cechy. Okazuje się jednak, że pisząc moduł dla Linuksa nie jesteśmy skazani na pisanie wszystkiego w C, gdyż można napisać go (prawie cały) w Haskellu.
Haskell jest językiem funkcyjnym, co oznacza, że styl pisania jest kompletnie inny niż w przypadku języków imperatywnych (np C). Doświadczenie pokazuje, że języki funkcyjne pozwalają nierzadko na o wiele krótsze i mniej narażone na błędy programowanie. Nie bez znaczenia jest, że weryfikacja programów funkcyjnych jest prostsza.
Jak się można domyślić włączanie kodu napisanego w Haskellu nie jest wspierane przez żaden specjalny mechanizm po stronie jądra. Żeby pisać w Haskellu moduły jądra można:
Żeby zacząć tworzenie modułu w Haskellu (a raczej z użyciem Haskella) potrzebujemy odpowiedniego środowiska. Standardowo tego języka programowania nie używa się do pisania kodu działającego inaczej niż poprzez system operacyjny (pod kontrolą systemu operacyjnego, a nie jako jego część). Z użyciem standardowego GHC możemy tworzyć kod tylko do wykonania w przestrzeni użytkownika. Co prawda dałoby się go uruchamiać w systemie poprzez mechanizm usermode helper, ale byłoby to kosztowne i bardzo nieeleganckie. Z tego powodu środowisko Haskella poprawia się do działającego w jądrze.
Stworzenie środowiska w rzeczywistości sprowadza się do skompilowania specjalnie przygotowanego GHC. Dostępne łaty zostały stworzone do wersji 6.8.2, dlatego będziemy kompilować tę wersję GHC.
Żeby móc skompilować GHC 6.8.2 musimy mieć poza GCC 4.4 i narzędziami wymaganymi do kompilacji (jak autotools) zainstalowane GHC 6.8.x, Happy, Alex.
Pierwszym krokiem będzie pozbawienie kodu wynikowego GHC wywołań systemowych z poziomu użytkownika itp., dlatego też potrzebna jest łata House. Z tym dodatkiem GHC jest w stanie skompilować Haskella do kodu działającego bezpośrednio na x86.
Możemy już tworzyć kod w x86, ale nie nadaje się on jeszcze do użycia z Linuksem, choćby dlatego, że zarządza pamięcią niezależnie od systemu i konfliktuje z obsługą sygnałów systemu. Z tego powodu konieczne jest nałożenie kolejnej łaty hghc, tym razem powoduje ona, że nasz wynikowy kod odwołuje się do odpowiednich wywołań systemowych.
Linki:
Wiemy już, że nie jest możliwe napisanie wszystkiego w Haskellu (ale prawie wszystko już tak), dlatego piszemy w nim funkcje, które zostaną wywołane przez przygotowany później specjalnie w tym celu plik .c. Nasz plik .hs musi również zawierać pewne nadmiarowe linie wskazujące by wyeksportować te funkcje poza ten plik. W pliku .c, natomiast, skorzystamy z dostępności makr i struktur jądra, ustawimy np init modułu i wywołamy funkcje dostarczane przez nasz plik haskellowy.
hsHello.hs:
{-# LANGUAGE ForeignFunctionInterface #-}
module Safe where
import Foreign.C.Types
import Foreign.C.String
hello :: IO CInt
hello = newCString "hello" >>= printk >> return 0
goodbye :: IO CInt
goodbye = newCString "Goodbye" >>= printk >> return 0
foreign export regparm3 hello :: IO CInt
foreign export regparm3 goodbye :: IO CInt
foreign import regparm3 unsafe printk :: CString -> IO Int
$HOUSE_DIR/ghc-6.8.2/compiler/stage1/ghc-inplace -B$HOUSE_DIR/ghc-6.8.2 hsHello.hs -c
Plik hsHello_stub.h:
#include "HsFFI.h"
#ifdef __cplusplus
extern "C" {
#endif
extern HsInt32 __attribute__((regparm (3))) hello();
extern HsInt32 __attribute__((regparm (3))) goodbye();
#ifdef __cplusplus
}
#endif
Plik hsHello_stub.c z już dodanymi ręcznie wywołaniami uruchamiającymi Haskella:
#define IN_STG_CODE 0
#include "Rts.h"
#include "Stg.h"
#ifdef __cplusplus
extern "C" {
#endif
extern StgClosure Safe_zdfhellozuaHn_closure;
HsInt32 __attribute__((regparm (3))) hello()
{
Capability *cap;
HaskellObj ret;
HsInt32 cret;
>>>
startupHaskell(0, NULL, NULL);
<<<
cap = rts_lock();
cap=rts_evalIO(cap,rts_apply(cap,(HaskellObj)runIO_closure,&Safe_zdfhellozuaHn_closure) ,&ret);
rts_checkSchedStatus("hello",cap);
cret=rts_getInt32(ret);
rts_unlock(cap);
>>>
hs_exit_nowait()
<<<
return cret;
}
extern StgClosure Safe_zdfgoodbyezuaHm_closure;
HsInt32 __attribute__((regparm (3))) goodbye()
{
Capability *cap;
HaskellObj ret;
HsInt32 cret;
>>>
startupHaskell(0, NULL, NULL);
<<<
cap = rts_lock();
cap=rts_evalIO(cap,rts_apply(cap,(HaskellObj)runIO_closure,&Safe_zdfgoodbyezuaHm_closure) ,&ret);
rts_checkSchedStatus("goodbye",cap);
cret=rts_getInt32(ret);
rts_unlock(cap);
>>>
hs_exit_nowait()
<<<
return cret;
}
static void stginit_export_Safe_zdfhellozuaHn() __attribute__((constructor));
static void stginit_export_Safe_zdfhellozuaHn()
{getStablePtr((StgPtr) &Safe_zdfhellozuaHn_closure);}
static void stginit_export_Safe_zdfgoodbyezuaHm() __attribute__((constructor));
static void stginit_export_Safe_zdfgoodbyezuaHm()
{getStablePtr((StgPtr) &Safe_zdfgoodbyezuaHm_closure);}
#ifdef __cplusplus
}
#endif
$hghc to wywołanie ghc ze sporą liczbą flag i opcji, dla czytelności zadeklarowano alias. Wartość $hghc można zobaczyć w np pliku install.
$hghc hsHello_stub.c -c
hello.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
// #include "hsHello_stub.h"
MODULE_LICENSE("Dual BSD/GPL");
#define ALLOC_MODE GFP_ATOMIC // GFP_KERNEL
extern int __attribute__((regparm(3))) hello();
extern int __attribute__((regparm(3))) goodbye();
// This is needed for the RTS - don't delcare it regparm3!
void __attribute__((regparm(0))) *malloc(size_t sz)
{
int i;
void *p;
// printk("Attemping to kmalloc 0x%x bytes\n", sz);
p = kmalloc(sz, ALLOC_MODE);
i = printk("Acquired pointer of 0x%p to 0x%x bytes\n", p, sz);
return p;
}
/*
(...) free, realloc, calloc - analogicznie
*/
void __assert_fail(void)
{
}
static int hello_init(void)
{
int h;
printk(KERN_ALERT "Hello C world\n");
h = hello();
printk(KERN_ALERT "Haskell gave us: %d\n", h);
return 0;
}
static void hello_exit(void)
{
int g;
g = goodbye();
printk(KERN_ALERT "Haskell gave us: %d\n", g);
printk(KERN_ALERT "Goodbye C world\n");
}
module_init(hello_init);
module_exit(hello_exit);
Makefile:
obj-m := module.o
module-objs := hello.o
i komenda:
make -C /usr/src/linux-2.6.26 M=`pwd` modules
ld -r -m elf_i386 -o module.ko hsHello_stub.o hsHello.o module.mod.o hello.o *.a libcbits.a
Haskell nie może podać makra KERN_EMERG do printk dlatego żeby zobaczyć czy zadziałało printk z poziomu kodu Haskella zaglądamy do dmesg
Już samo stworzenie modułu w Haskellu jest bardzo utrudnione z powodu niedoskonałości narzędzi oraz konieczności wykonywania zmian w kodzie. Niestety kod generowany przez ghc po dodanych łatach też posiada pewne wady. Znanym problemem jest, że House-GHC nie zwalnia całej zaalokowanej pamięci, co w najlepszym razie kończy się tylko sporym wyciekiem pamięci, a w najgorszym powoduje błąd w systemie.
Nie ma w tym nic dziwnego, że skoro GHC z House umie tworzyć kod działający bezpośrednio na x86, to można z jego użyciem tworzyć system operacyjny. Co więcej w takim systemie unika się problemów z łączeniem kodu z C z kodem Haskella. Taki system powstał, jest to bardziej proof of concept niż faktyczny system, ale udało się w 2005 poprowadzić prezentację na komputerze działającym pod tym systemem. Jest też nowa gałęź House dodająca Lightweight Concurrency framework i planistę opartego na priorytetach o nazwie Lighthouse.
Odnośniki:
Janina Mincer-Daszkiewicz |