Регистрация | Войти
Lisp — программируемый язык программирования

Системные вызовы Linux

1)

Рассмотрим такую полезную вещь, как системные вызовы в ОС Linux. Фактически это интерфейс, который предоставляет ОС (ядро) для осуществления различных высокоуровневых действий: программа делает вызов, например "создать папку" или "перезагрузить компьютер", а ядро заботится о том, как конкретно это сделать (или НЕ сделать, если у программы недостаточно прав).

Всего вызовов насчитывается около 280. Все они имеют номера, которые перечислены в файле unistd.h (его местоположение отличается на разных системах, скорее всего это /usr/include/asm-[x86|x86_64|...]/unistd.h). Кроме того, в динамической библиотеке libc.so.6 имеются обвертки для всех этих вызовов.

В стандарт CL не входит какого-либо способа сделать системный вызов, или вызвать функцию из бинарной библиотеки. Однако, до библиотечных функций в .so можно добраться с помощью FFI. Так что мы могли бы обращатся к libc.so за каждым отдельным вызовом - а их около 300 штук. Вместо этого мы поступим иначе - воспользуемся функцией syscall из libc.so - она делает системный вызов, который делает системные вызовы :) Например - создать папку можно с помощью функции mkdir:

mkdir("new path", 0777);

А можно так:

syscall(83, "new path", 0777); // где 83 - это номер системного вызова, который создает папку.

У второго варианта есть преимущество - с помощью него можно сделать вызов, только появившийся в ядре, но не описанный в отдельную функцию. Характер возвращаемого значения разный для разных системных вызовов - для одних это просто флаг вышло/не вышло (ноль/не ноль), а для других какое-то значение, имеющее смысл - например дескриптор файла или устройства.

Среди системных вызовов есть как простые, так и достаточно интересные и функциональные. Например fork, для создания точной копии текущего процесса, или exec для запуска программы на исполнение, есть даже system для обрашения к shell-у.

Таким образом, три кита на которых основывается Linux, то есть системные вызовы, библиотечные функции и обособленные программы, каждая из которых выполняет свою роль - всё это доступно в Common Lisp, причём без особого труда. К делу.

Переводим unistd.h на Лисп

Итак, для начала нужно подобрать вменяемые названия для номеров вызовов, как это сделано в unistd.h:

#define __NR_read             0
#define __NR_write 1
#define __NR_open 2
// ...
#define __NR_tee 276
#define __NR_sync_file_range 277
#define __NR_vmsplice 278

Можно это сделать вручную, а можно, например, с помощью регулярных выражений:

(require 'cl-ppcre)

(in-package #:cl-ppcre)

(defun unistd-to-lisp (unistd.h unistd.lisp)
    (let ((in (open unistd.h :if-does-not-exist nil)) (out (open unistd.lisp :direction :output :if-exists :supersede)))
        (when in
            (loop for line = (read-line in nil)
                while line do (if (scan "#define\\s+__NR_[a-z]+\\s+[0-9]+" line)
                                  (format out "~a~%"
                                              (regex-replace "#define\\s+__NR_([a-z]+)\\s+([0-9]+).*" line "(defconstant +\\1 \\2)")
)
)
)

            (close in)
            (close out)
)
)
)


;; И общий способ перевода однострочных объявлений #define из .h файлов такой же:
(defun h-to-lisp (f.h f.lisp)
    (let ((in (open f.h :if-does-not-exist nil)) (out (open f.lisp :direction :output :if-exists :supersede)))
        (when in
            (loop for line = (read-line in nil)
                while line do (if (scan "#define\\s+\\S+\\s+\\S+" line)
                                  (format out "~a~%"
                                              (string-downcase  ;; уменьшаем регистр.
                                               (substitute #\- #\_  ;; Вместо _ ставим - , конечно.
                                                 (regex-replace "#\\s*define\\s+(\\S+)\\s+(\\S+).*" line "(defconstant +\\1+ \\2)")
)
)
)
)
)

            (close in)
            (close out)
)
)
)

Теперь пишем что-то вроде:

(unistd-to-lisp "/usr/include/asm-x86_64/unistd.h" "unist.lisp")

И имеем файл следующего содержания:

(defconstant +read 0)
(defconstant +write 1)
(defconstant +open 2)
;;; ...
(defconstant +splice 275)
(defconstant +tee 276)
(defconstant +vmsplice 278)

2)

Который можно тут же и загрузить:

(load "unistd.lisp")

Загружаем библиотеку стандартных функций

Теперь наша задача - найти рабочую библиотеку стандартных функций. Попробуйте набрать в SBCL (далее по тексту до части CFFI - всё про SBCL) следующую строку:

* (load-shared-object "libc.so")

Скорее всего начнётся ругань на заголовок ELF. С чего бы это? А вы посмотрите cat /ust/lib/libc.so... Там видим, где лежит libc.so.6, так что пробуем так:

* (load-shared-object "libc.so.6")

Должно сработать, в крайнем случае - можно пойти в корень и поискать нормальную библиотеку : find / -name "libс.so.6", и вводить уже полный путь до неё. Также можно использовать стандартную библиотеку C++, которая сожержит всё то же самое, плюс ещё кое-что:

* (load-shared-object "libstdc++.so.6")

Check : это работает?

После того, как стандартная библиотека загружена (что потребовало одной строчки), можно сделать любой системный вызов, даже не придется объявлять никаких си-типов или функций. Итак, однострок:

* (alien-funcall (extern-alien "syscall" (function int int c-string int)) +mkdir "new_path" 0777)

должна появиться папка в домашней директории (ну, точнее в той директории, которая была текущей у родительского процесса в момент создания лисп-процесса).

* (alien-funcall (extern-alien "strlen" (function int c-string)) "12345")

Должна вернуть 5.

Пищем обвёртку

Строчки выше не очень удобные - приходится прописывать очевидные типы аргументов, ведь используются только числа и строки. Хотелось бы просто получить адрес входа в код функции, и кидать на неё аргументы.

Определим пакет ffi-sbcl, в котором будут лежать нужные для работы с ffi функции:

(defpackage #:ffi-sbcl
  (:use #:common-lisp #:sb-alien)
  (:export
     #:ff-call
  
)
)


(in-package #:ffi-sbcl)

(defun ffi-type (arg)
  "Возвращает для аргумента предположительный ffi-тип.
   Тут, конечно, не все типы."

  (cond ((integerp arg) 'int)
        ((stringp  arg) 'c-string)
        ((floatp   arg) 'double-float)
        ((listp    arg) 'array)
        ((null     arg) 'void)
)
)


(defun ffi-types-list (args)
  "Возвращает список ffi-типов для списка аргументов.
   Использует функцию ffi-type."

  (loop for arg in args by #'cdr
        collect (ffi-type arg)
)
)


(defmacro %%ff-call (name types fargs ret-type)
  "Этот макрос обварачивает самый простой вызов ffi-функции."
  `(alien-funcall
    (extern-alien ,name (function ,ret-type ,@types))
     ,@fargs
)
)


(defmacro %%ff-call-on-args (name types fargs ret-type ret-arg)
  "Этот вызов ffi-функции возвращает не только результат,
   но и все аргументы, которые могли быть модифицированны."

  `(let ((result
         (alien-funcall
          (extern-alien ,name (function ,ret-type ,@types))
           ,@fargs
)
)
)

     (list result (nth ,ret-arg ,fargs))
)
)


(defmacro %ff-call (name args ret-type ret-arg)
  (multiple-value-bind (types)
                (ffi-types-list args)
    (if ret-arg
      `(%%ff-call-on-args ,name ,types ,args ,ret-type)
      `(%%ff-call ,name ,types ,args ,ret-type)
)
)
)


(defun ff-call (name ret-type ret-arg &rest args)
  (eval (%ff-call name args ret-type ret-arg))
)

Теперь напишем функцию sys-call, которую положим в пакет linux:

(defpackage #:linux
  (:use #:cl #:ffi-sbcl)
  (:export
     #:sys-call
  
)
)


(in-package #:linux)

(load "unistd.lisp")

(defun sys-call (num &key (ret-arg nil) &rest args)
  (ff-call "syscall" int ret-arg (cons num args))
)

По мере появления каких-то функций их можно сюда дописывать.

Shell в Лиспе

Чтобы использовать системные вызовы нужно знать как они устроены - это можно разузнать из man-ов, также могут помочь некоторые заголовочные файлы. Чтобы не переключатся постоянно между лиспом и командной строкой - встроим shell в lisp!

Рассмотрим функцию system :

int system(const char *command);

эта функция, можно сказать, принимает строку, а далее исполняет её с помощью интерпретатора shell (а именно /bin/sh -c ...). Результат пишется на дескриптор 1, т.е. на stdout. Это не системный вызов, но это функция из libc.so.6, поэтому:

(defun shell (str)
  (ff-call "system" str int nil)
)

даже через рубежи emacs-а и slima-а мы увидим :

(shell "ls -al")
...

для ман-страниц можно сделать отдельную функцию:

(defun man (subj &optional (n ""))
  (shell (concatenate 'string "man " n subj))
)


(man "fork" 2)
;; выводит man про fork()

Практика : делаем системные вызовы

Любой вызов теперь доступен - открываем unistd.lisp, выбираем нужный, смотрим, если нужно, (man "имя-вызова" 2) или (man имя-вызова 3). Вот некоторые примеры:

Файлы

В unix файлы это не только "файлы", evrything is a file, как гласит основной принцип. Это значит, что файлы - и директории, и устройства, и структуры данных ОС (например /proc), даже модем - тоже файл (особенно когда он не может обнаружится :)). Эта система собственно и начиналась как файловая система, а системные вызовы для работы с файлами имеют самые первые номера:

(defconstant +read 0)
(defconstant +write 1)
(defconstant +open 2)
(defconstant +close 3)
; ...
(defconstant +creat 85)
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int close(int fd);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

open открывает/создает файл/устройство по имени pathname, flags может представлять из себя сложную комбинацию битовых флагов, а mode комбинируется (mode & umask) с umask процесса для задания прав при создании нового файла. creat эквивалентен open с flags, равными O_CREAT | O_WRONLY | O_TRUNC. close просто закрывает дескриптор - все блокировки снимаются. write (и read) пишет (читает) буфер (в буфер) на файловый дескриптор.

Вобщем, чтобы не возится с числами, нам опять понадобится h-to-lisp:

(h-to-lisp "/usr/include/bits/fcntl.h" "fcntl.lisp")
(h-to-lisp "/usr/include/bits/stat.h" "stat.lisp")

должны получится определения битовых флагов O_*, F_*, FD_* и S_*, иначе - нужно искать такие же файлы в других директориях.

Хорошо, что syscall в этих случаях возвращает дескрипторы - только используя её одну можно писать и читать файлы:

(defun open! (path &key (flags +o-rdwr+) (mode +s-irwxu+))
  (sys-call +open path flags mode)
)


(defun create! (path &key (mode +s-irwxu+))
  (sys-call +create path mode)
)


(defun close! (fid)
  (sys-call +close fid)
)


; Я ограничился записью обычного текста
; в общем можно писать любые данные
(defun write! (fid str)
  (sys-call +write fid str (length str))
)


; Вернет список - результат, и считанную строку
(defun read! (fid coun)
  (let* ((str (string ""))
         (result (sys-call +read fid str coun :ret-arg t))
)

    (list (nth 0 result) (nth 2 result))
)
)


(write! (setf fid (create! "new_file.txt" #o777)) "qwerty")

Как не странно - файлы создаются, буквы в них появляются :)

/dev

Все файлы устройств находятся в директории /dev. Например при смонтированном cd-rom-е с CD диском можно получить дескриптор файла /dev/cdrom, и прочитать данные с него на жесткий диск.

(h-to-lisp "/usr/include/linux/cdrom.h" "cdrom.lisp")
;;  тут должен быть пример конвертации cda -> wav.

/proc

Статистика о файлах

Далее по списку следуют вызовы которые возвращают статистику о файлах:

(defconstant +stat 4)
(defconstant +fstat 5)
(defconstant +lstat 6)
int stat(const char *file_name, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *file_name, struct stat *buf);

stat и lstat мало отличаются - их первый аргумент это путь к файлу/устройству (lstat не переходит по символьным ссылкам), первый аргумент fstat - дескриптор файла, который мы можем получить. Эти три функции относятся к типу функций, изменяющих свои аргументы - статистика передается через второй аргумент, кроме того он является указателем на структуру типа stat.

Сама структура выглядит так:

struct stat {
dev_t st_dev; /* устройство */
ino_t st_ino; /* inode */
mode_t st_mode; /* режим доступа */
nlink_t st_nlink; /* количество жестких ссылок */
uid_t st_uid; /* ID пользователя-владельца */
gid_t st_gid; /* ID группы-владельца */
dev_t st_rdev; /* тип устройства */
/* (если это устройство) */
off_t st_size; /* общий размер в байтах */
unsigned long st_blksize; /* размер блока ввода-вывода */
/* в файловой системе */
unsigned long st_blocks; /* количество выделенных блоков */
time_t st_atime; /* время последнего доступа */
time_t st_mtime; /* время последней модификации */
time_t st_ctime; /* время последнего изменения */
};

так что ту не обойтись без её описания на ffi.

Директории

(defconstant +getcwd 79)
(defconstant +chdir 80)
(defconstant +fchdir 81)
(defconstant +rename 82)
(defconstant +mkdir 83)
(defconstant +rmdir 84)
char *getcwd(char *buf, size_t size);
int chdir(const char *path);
int fchdir(int fd);
int rename(const char *oldpath, const char *newpath);
int mkdir(const char *pathname, mode_t mode);
int rmdir(const char *pathname);
;; Так можно создать папку с заданными правами:
(defun mkdir! (path &optional (mode #o777))
  (sys-call +mkdir path mode)
)


;; И удалить её:
(defun rmdir! (path)
  (sys-call +rmdir path)
)


;; Но можно перед этим сменить текущую
(defun cd! (path)
  (sys-call +chdir path)
)

fork и exec

reboot и shutdown

int reboot(int magic, int magic2, int cmd, void *arg);

При наличии прав root можно это сделать. Понадобятся значения "магических чисел" из reboot.h, так что опять:

(h-to-lisp "/usr/include/linux/reboot.h" "reboot.h")
(defun reboot! () (sys-call +reboot +linux-reboot-magic1+ +linux-reboot-magic2+ +linux-reboot-cmd-restart+ ""))
(defun shutdown! () (sys-call +reboot +linux-reboot-magic1+ +linux-reboot-magic2+ +linux-reboot-cmd-halt+ ""))

;; теперь попробуйте:
(reboot!)

Дополнения

Переносимость - CFFI

Функция sys-call обращается к ff-call из ffi-sbcl, можно таким же образом оптределить ff-call для других реализаций, но проще воспользоваться CFFI, и определить ff-call через функцию foreign-funcall того пакета.

Как собрать .so

Пример динамической библиотеки, экспортирующей syscall. Делается это дело примерно так:

файл syscall.c:

#include <sys/syscall.h>
int system_call(int n_syscall, void *args[], int n_args) {
switch (n_args) {
case 0: return syscall(n_syscall);
case 1: return syscall(n_syscall, args[0]);
case 2: return syscall(n_syscall, args[0], args[1]);
case 3: return syscall(n_syscall, args[0], args[1], args[2]);
case 4: return syscall(n_syscall, args[0], args[1], args[2], args[3]);
case 5: return syscall(n_syscall, args[0], args[1], args[2], args[3], args[4]);
}
return -1;
}

компилируем:

gcc -c -fPIC syscall.c -o syscall.o
gcc -shared syscall.o -o syscall.so
rm -f syscall.o

таким же образом можно написать и скомпилировать в .so любой функционал на Си для использования его в Лиспе.

1) Поправка - это статья из серии "сделаем велосипед", в Real World лучше использовать CFFI и IOLib
2)+ тут означает константу. Кстати, может кто-то объяснить, как в строку вставлять табуляции и тому подобное, по типу "—\t—\n"?
@2009-2013 lisper.ru