poniedziałek, 19 lipca 2010

Wywołania systemowe w Linuksie

1. Prolog.

Witam. W tym artykule przedstawię wam czym są wywołania systemowe w Linuksie, w programowaniu w C. Zakładam że czytelnicy znają zasady kompilowania programów, a także znają podstawy programowania w C. Do każdej funkcji jest napisany manual więc warto je czytać. A więc czas zacząć.

2. Co to są wywołania systemowe?

Wywołania systemowe w C to funkcje które aby zostały wykonane, muszą odnieść
się do funkcji jądra systemowego. Po poprawnym wykonaniu zwracają wartość 0,
zaś po błędzie zwracane jest -1 i odpowiednio ustawiania zmienna errno (ale nie zawsze). Zmienna
errno przyjmuje konkretne wartości, dzięki którym możemy się dowiedzieć co
było przyczyną nie wykonania się naszej aplikacji.

3. Otwarcie i utworzenie pliku

Służą do tego dwie funkcje: open() i create(). Obie zwracają deskryptor pliku,
do którego mogą odnosić się inne funkcje, np. read(). open() może być
wywoływana na dwa różne sposoby. Oto pierwszy z nich:
int open(const char *pathname, int flags);
Argument pathname jest ścieżką do pliku a flags stałą lub stałymi które mówią
jak ma być otwarty plik. Flaga musi być jedną z wartości O_RDONLY (tylko do
odczytu), O_WRONLY (tylko do zapisu) lub O_RDWR (do zapisu i odczytu). Istnieją także inne flagi które możecie poznać czytając manuala systemowego OPEN(2).
Flagi można zORować, tzn.połączyć
operatorem |.
Tak przedstawia się drugi sposób:
int open(const char *pathname, int flags, mode_t mode);
pathname i flags bez zmian, zaś argument mode mówi z jakimi prawami ma być
utworzony plik jeżeli nie istnieje. Aby poznać wartości mode odysłam do manuala OPEN(2).
Została nam jeszcze funkcja creat() która tworzy plik i zwraca deskryptor. Oto jej prototyp:
int creat(const char *pathname, mode_t mode);
Chyba nie muszę tłumaczyć co oznaczają argumenty.
Wszystkie te funkcje zwracają wartość typu int, która jest małą liczbą
całkowitą, będącą deskryptorem. Gdy wyniknie błąd zwraca wartość -1 i jest
ustawiane errno.
Na koniec mały przykład:

#include "stdio.h"
#include "sys/stat.h"
#include "fcntl.h"

main()
{
int desk;
desk = open("/home/seprob/doc/file.txt", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
/* Zwróć deskryptor pliku /home/seprob/file.txt z prawami do odczytu i
* zapisu. Jeżeli plik nie istnieje to go stwórz i nadaj mu prawa
* odczytu i zapisu dla właściciela. */

if (desk != -1)
printf("Operacja powiodła się. Oto deskryptor pliku: %dn", desk);
else
printf("Operacja nie powiodła się. Sprawdź errno.n");

return 0;
}

4. Zmienianie praw dostępu do pliku

Zmienić prawa dostępu do pliku można zmienić dzięki dwóm funkcjom. Oto
pierwsza z nich:
int chmod(const char *path, mode_t mode);
Pierwszy argument funkcji chmod() to ścieżka do pliku, drugi to prawa jakie ma
posiadać plik. Zaprezentuje mały przykład:

[seprob@sepper trash]$ ls -l
razem 16
-rwxr-xr-x 1 seprob seprob 12003 paź 25 18:04 chmod*
-rw-r--r-- 1 seprob seprob 542 paź 25 18:04 chmod.c
-rw-r--r-- 1 seprob seprob 0 paź 25 17:51 file.txt

Jak widać plik file.txt ma prawa czytania i pisania dla użytkownika, czytania
dla grupy oraz czytania dla innych. Stosując poniższy kod:

#include "stdio.h"
#include "sys/stat.h"
#include "fcntl.h"

main()
{
int desk;
desk = open("/home/seprob/trash/file.txt", O_RDONLY);
/* Zwróc deskryptor pliku /home/seprob/trash/file.txt z prawami do
* odczytu */

if (desk != -1)
printf("open() powiodło się.n");
else {
printf("open() nie powiodło się.n");
exit(1); /* Funkcja exit() o której poźniej */
}

if ((fchmod(desk, S_IRUSR|S_IRGRP|S_IROTH)) != -1)
printf("fchmod() powiodło się.n");
else
printf("chmod() nie powiodło się. Sprawdź errno.n");

return 0;
}

Mam taki wynik:

[seprob@sepper trash]$ ./chmod
open() powiodło się.
fchmod() powiodło się.
[seprob@sepper trash]$ ls -l
razem 16
-rwxr-xr-x 1 seprob seprob 12003 paź 25 18:04 chmod*
-rw-r--r-- 1 seprob seprob 542 paź 25 18:04 chmod.c
-r--r--r-- 1 seprob seprob 0 paź 25 17:51 file.txt

Oba wywołania zwracają 0 gdy się powiedzie a -1 gdy jest błąd. Po więcej
informacji zapraszam do przeczytania manuala CHMOD(2).

5. Zmiana właściciela pliku

Zmienić właściciela pliku możemy dzięki dwóm funkcjom. Oto prototyp pierwszej
z nich:
int chown(const char *path, uid_t owner, gid_t group);
Pierwszy argument jest ścieżką do programu, drugi to UID użytkownika, a trzeci
to GID grupy. Aby sprawdzić UID i GID konretnego użytkownika należy zajrzeć
do pliku /etc/passwd. Oto przykład:

seprob:x:501:501:Bartłomiej Korpała:/home/seprob:/bin/bash

W tym przypadku UID uzytkownika seprob to 501 a GID także 501. Należy pamiętać
że właściciela pliku może zmienić tylko superużytkownik czyli root.
Drugim sposobem zmiany właściciela pliku jest użycie funkcji fchown(). Jak
łatwo się domyślić zamiast ścieżki do programu nalezy podać jego deskryptor,
ale ja podam jej prototyp:
int fchown(int fd, uid_t owner, gid_t group);
Reszta argumentów jest niezmienna.
Na koniec przykład:

#include "stdio.h"
#include "sys/types.h"

main()
{
chown("/home/seprob/trash/file.txt", 500, 500);
/* Zmień właściciela pliku na UID 500 i GID 500.
* W moim przypadku użytkownikiem o takim UIDzie i GIDzie jest
* gege:x:500:500:Grzegorz Kutyba:/home/gege:/bin/bash */

return 0;
}

Najpierw zlistujmy katalog:

[seprob@sepper trash]$ ls -l
razem 16
-rwxr-xr-x 1 seprob seprob 11456 paź 26 10:36 chown*
-rw-r--r-- 1 seprob seprob 290 paź 26 10:36 chown.c
-r--r--r-- 1 seprob seprob 0 paź 25 17:51 file.txt

Po użyciu naszego programu, listowanie ma taki wygląd:

[root@sepper trash]# ls -l
razem 16
-rwxr-xr-x 1 seprob seprob 11456 paź 26 10:36 chown*
-rw-r--r-- 1 seprob seprob 290 paź 26 10:36 chown.c
-r--r--r-- 1 gege gege 0 paź 25 17:51 file.txt

Bez zmian; po błędzie zwracane jest -1 a po sukcesie 0.

6. Uruchamianie programu

Można do tego wykorzystać funkcje execve():
int execve(const char *filename, char *const argv[], char *const envp[]);
Pierwszy argument jest ścieżką do pliku, następne dwa są argumentami wywołania
programu, do niego przekazywanymi. Jeżeli do programu nie przekazujemy
żadnych argumentów, to argv i envp oznaczamy jako NULL. execve() po sukcesie
nie wraca, program wywoływany dziedziczy PID po programie który go wywołuje.
Argument argv i envp musi zawsze być zakończony znacznikiem NULL.
Najlepiej zilistruje to przykład, w którym uruchimimy poprzez funkcję execve()
program który zmienia właściciela pliku, z poprzedniej sekcji:

[seprob@sepper trash]$ ls -l
razem 32
-rwxr-xr-x 1 seprob seprob 11456 paź 26 10:36 chown*
-rw-r--r-- 1 seprob seprob 290 paź 26 10:36 chown.c
-rwxr-xr-x 1 seprob seprob 11612 paź 26 13:34 execve*
-rw-r--r-- 1 seprob seprob 213 paź 26 13:34 execve.c
-rw-r--r-- 1 root root 0 paź 26 13:30 file.txt

Jak widać właścicielem jest root. Po odpaleniu tego programu:

#include "stdio.h"
#include "unistd.h"

main()
{
execve("/home/seprob/trash/chown", NULL, NULL);

return 0;
}

Listing przedstawia się nastepująco:

[root@sepper trash]# ./execve
[root@sepper trash]# ls -l
razem 32
-rwxr-xr-x 1 seprob seprob 11456 paź 26 10:36 chown*
-rw-r--r-- 1 seprob seprob 290 paź 26 10:36 chown.c
-rwxr-xr-x 1 seprob seprob 11442 paź 26 13:37 execve*
-rw-r--r-- 1 seprob seprob 114 paź 26 13:37 execve.c
-rw-r--r-- 1 gege gege 0 paź 26 13:30 file.txt

7. Zakończenie programu

Służy do tego funkcja exit(), której prototyp przedstawia się następująco:
void exit(int status);
Argument status jest jednym ze stałych EXIT_SUCCES i EXIT_FAILURE. Pierwsza
ma wartość 0, druga 1. Można uzyć stałej lub jednej z liczb, odpowiednio przy
powrawnym zakończeniu programu, i błędnym. Zaobserwować można to na
przykładzie:

#include "stdio.h"
#include "sys/stat.h"
#include "fcntl.h"

main()
{
int desk;
desk = open("/home/seprob/trash/file.txt", O_RDONLY);

if (desk != -1)
printf("open() powiodło się.n");
else {
printf("open() nie powiodło się.n");
exit(1);
}

if ((fchmod(desk, S_IRUSR|S_IRGRP|S_IROTH)) != -1) {
printf("fchmod() powiodło się.n");
exit(0);
}
else
printf("chmod() nie powiodło się. Sprawdź errno.n");

return 0;
}

Funkcja nie zwraca żadnej wartości. Wszystkie otwarte strumienie plików są
zamykane, a także są opróżniane ich bufory.

8. Wysyłanie sygnału do procesu

Można do tego użyć funkcji kill():
int kill(pid_t pid, int sig);
Pierwszy argument do numer PID procesu, drugi to sygnał który chcemy wysłać do procesu. Aby dowiedzieć się
jaki numer PID ma konkretny proces należy uruchomić program ps, najlepiej z opcjami -aux. Aby to zrozumieć pokażę przykład:

[seprob@sepper seprob]$ ps -aux
Warning: bad syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.0 1468 64 ? S 12:58 0:06 init
seprob 1817 2.4 4.3 60800 5460 ? S 13:44 0:59 xmms

Oczywiście obciąłem pełna listę procesów, żeby nie zajmować nie potrzebnie miejsca. Jak widać xmms który
będę chciał wyłączyć ma numer PID 1817. A teraz wypiszemy listę sygnałów:

[seprob@sepper seprob]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 35) SIGRTMIN 36) SIGRTMIN+1
37) SIGRTMIN+2 38) SIGRTMIN+3 39) SIGRTMIN+4 40) SIGRTMIN+5
41) SIGRTMIN+6 42) SIGRTMIN+7 43) SIGRTMIN+8 44) SIGRTMIN+9
45) SIGRTMIN+10 46) SIGRTMIN+11 47) SIGRTMIN+12 48) SIGRTMIN+13
49) SIGRTMIN+14 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8
57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4
61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX

Aby wyłaćzyć xmmsa skompiluje i uruchomie ten o to program:

#include "stdio.h"
#include "sys/types.h"
#include "signal.h"

main()
{
int killer;
killer = kill(1817, SIGQUIT);
/* Wyslij do procesu o numerze PID 1817 sygbal SIGQUIT */

if (killer != -1)
printf("Xmms wylaczony.n");
else {
printf("Blad.n");
exit(1);
}

return 0;
}

Po sukcesie program zwraca 0 i po błędzie -1.

9. Tworzenie i usuwanie katalogu

Najpierw omówimy tworzenie katalogu. Stworzona do tego została funkcja mkdir(). Oto jej prototyp:
int mkdir(const char *pathname, mode_t mode);
Pierwszy argument to ścieżka gdzie ma być utworzony katalog i jego nazwa, drugi zaś to prawa z jakimi
ma byc stworzony. Są to te same tryby jak w funkcji create(). Potrzebny jest przykład:

[seprob@sepper trash]$ ls
chown* chown.c execve* execve.c file.txt mkdir*

A teraz zastosujmy poniższy kod:

#include "sys/stat.h"
#include "fcntl.h"

main()
{
int create;
create = mkdir("/home/seprob/trash/new_dir", S_IRWXU);
/* Stworz katalog /home/seprob/trash/new_dir z prawami odczytu i zapisu dla
* wlasciciela. */

if (create != -1)
printf("mkdir() sie powiodlo.n");
else {
printf("Blad.n");
exit(1);
}

return 0;
}

Oto wynik:

[seprob@sepper trash]$ ./mkdir
mkdir() sie powiodlo.
[seprob@sepper trash]$ ls
chown* chown.c execve* execve.c file.txt mkdir* new_dir/

Funkcja usuwająca katalogi to rmdir(). Różni się tylko tym od mkdir() że nie ma drugiego argumentu mode,
i oczywiście usuwa a nie tworzy katalogi. Oba wywołania zwracają 0 po sukcesie i -1 po błędzie.

10. Czytanie z deskryptora pliku

Oto funkcja która to umożliwia:
ssize_t read(int fd, void *buf, size_t count);
Pierwszym argumentem jest deskryptor pliku, ktory nalezy ustawic do czytania. Drugi to bufor gdzie ma byc
przechowywany przeczytany ciag znakow. Ostatnim jest maksymalna ilosc bajtow do przeczytania.
Jak zwykle musimy to zaimplementować w C ;-) :

Chcemy przeczytac zawartość pliku /home/seprob/trash/file.txt który ma taką zawartość:

[seprob@sepper others]$ cat /home/seprob/trash/file.txt
Seprob is a master of programming ;-)

A teraz kod który umożliwi na to samo co polecenie cat:

#include "stdio.h"
#include "unistd.h"
#include "sys/stat.h"
#include "fcntl.h"

main()
{
char buf[150];
int fd;

fd = open("/home/seprob/trash/file.txt", O_RDONLY);

if ((read(fd, buf, 200)) != -1) {
printf("read() powiodlo sie.n");
printf("Przeczytany lancuch znakow: %s", buf);
}
else
printf("read() sie nie powiodlo. Sprawdz errno.n");

return 0;
}

I wynik:

[seprob@sepper others]$ ./read
read() powiodlo sie.
Przeczytany lancuch znakow: Seprob is a master of programming ;-)

Program zwraca ilość przeczytanych znaków jeżeli się powiedzie, i -1 gdy jest błąd.

11. Epilog

Myślę że dobrze wszystko wyłumaczyłem. Oczywiście jest to tylko niewielka część z całego wachlarza wywołań systemowych.
Cały spis funkcji możecie znaleźć na stronie: http://www.linuxpl.org/LPG/node191.html