Эта глава открывает большую и очень важную для Linux-программиста тему многозадачности. Описать все сразу не получится, поэтому мы будем неоднократно возвращаться к многозадачности в последующих главах книги. Пристегните ремни покрепче!
Наберите в своей оболочке следующую команду:
$ ps -e
На экран будут выведен список всех работающих в системе процессов. Если хотите
посчитать количество процессов, наберите что-нибудь, набодобие этого:
$ ps -e --no-headers | nl | tail -n 1
74 4650 pts/0 00:00:00 tail
$
Первое число - это количество работающих в системе процессов. Пользователи KDE могут воспользоваться программой kpm, а пользователи Gnome - программой gnome-system-monitor для получения информации о процессах. На то он и Linux, чтобы позволять пользователю делать одно и то же разными способами.
Возникает вопрос: "Что такое процесс?". Процессы в Linux, как и файлы, являются аксиоматическими понятиями. Иногда процесс отождествляют с запущенной программой, однако это не всегда так. Будем считать, что процесс - это рабочая единица системы, которая выполняет что-то. Многозадачность - это возможность одновременного сосуществования нескольких процессов в одной системе.
Linux - многозадачная операционная система. Это означает что процессы в ней работают одновременно. Естественно, это условная формулировка. Ядро Linux постоянно переключает процессы, то есть время от времени дает каждому из них сколько-нибудь процессорного времени. Переключение происходит довольно быстро, поэтому нам кажется, что процессы работают одновременно.
Одни процессы могут порождать другие процессы, образовывая древовидную структуру. Порождающие процессы называются родителями или родительскими процессами, а порожденные - потомками или дочерними процессами. На вершине этого "дерева" находится процесс init, который порождается автоматически ядром в процесссе загрузки системы.
К каждому процессу в системе привязана пара целых неотрицательных чисел: идентификатор процесса PID (Process IDentifier) и идентификатор родительского процесса PPID (Parent Process IDentifier). Для каждого процесса PID является уникальным (в конкретный момент времени), а PPID равен идентификатору процесса-родителя. Если ввести в оболочку команду ps -ef, то на экран будет выведен список процессов со значениями их PID и PPID (вторая и третья колонки соотв.). Вот, например, что творится у меня в системе:
$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 06:16 ? 00:00:01 init [3]
root 2 1 0 06:16 ? 00:00:00 [migration/0]
root 3 1 0 06:16 ? 00:00:00 [ksoftirqd/0]
root 4 1 0 06:16 ? 00:00:00 [watchdog/0]
root 5 1 0 06:16 ? 00:00:00 [migration/1]
root 6 1 0 06:16 ? 00:00:00 [ksoftirqd/1]
root 7 1 0 06:16 ? 00:00:00 [watchdog/1]
root 8 1 0 06:16 ? 00:00:00 [events/0]
root 9 1 0 06:16 ? 00:00:00 [events/1]
root 10 1 0 06:16 ? 00:00:00 [khelper]
root 11 1 0 06:16 ? 00:00:00 [kthread]
root 35 11 0 06:16 ? 00:00:00 [kblockd/0]
root 36 11 0 06:16 ? 00:00:00 [kblockd/1]
root 37 11 0 06:16 ? 00:00:00 [kacpid]
root 216 11 0 06:16 ? 00:00:00 [kseriod]
root 244 11 0 06:16 ? 00:00:00 [pdflush]
root 245 11 0 06:16 ? 00:00:00 [pdflush]
root 246 11 0 06:16 ? 00:00:00 [kswapd0]
root 247 11 0 06:16 ? 00:00:00 [aio/0]
root 248 11 0 06:16 ? 00:00:00 [aio/1]
root 395 11 0 06:16 ? 00:00:00 [ata/0]
root 396 11 0 06:16 ? 00:00:00 [ata/1]
root 397 11 0 06:16 ? 00:00:00 [ata_aux]
root 407 11 0 06:16 ? 00:00:00 [scsi_eh_0]
root 408 11 0 06:16 ? 00:00:00 [scsi_eh_1]
root 409 11 0 06:16 ? 00:00:00 [scsi_eh_2]
root 410 11 0 06:16 ? 00:00:00 [scsi_eh_3]
root 422 11 0 06:17 ? 00:00:00 [scsi_eh_4]
root 423 11 0 06:17 ? 00:00:00 [scsi_eh_5]
root 1406 11 0 06:17 ? 00:00:00 [kjournald]
root 1443 11 0 06:17 ? 00:00:00 [ksuspend_usbd]
root 1446 11 0 06:17 ? 00:00:00 [khubd]
root 1462 1 0 06:17 ? 00:00:00 /sbin/udevd --daemon
root 3230 11 0 06:17 ? 00:00:00 [kpsmoused]
nn 3498 11591 0 12:06 pts/1 00:00:08 kate 006.html
nn 3984 11591 0 12:08 pts/1 00:00:03 kate 007.html
nn 4026 1 0 12:09 ? 00:00:00 kio_uiserver [kdeinit]
nobody 4563 6054 0 09:15 ? 00:00:00 /usr/sbin/httpd -k start
root 4652 11 0 06:17 ? 00:00:00 [khpsbpkt]
root 4785 11 0 06:17 ? 00:00:00 [pccardd]
root 4786 11 0 06:17 ? 00:00:00 [tifm/0]
root 4840 11 0 06:17 ? 00:00:00 [knodemgrd_0]
root 4849 11 0 06:17 ? 00:00:00 [kmmcd]
nn 5504 6133 0 12:17 ? 00:00:00 konsole [kdeinit]
nn 5505 5504 0 12:17 pts/0 00:00:00 /bin/bash
root 5807 11 0 06:17 ? 00:00:00 [kjournald]
root 5810 11 0 06:17 ? 00:00:00 [kjournald]
root 5970 1 0 06:17 ? 00:00:00 /usr/sbin/syslog-ng
root 5973 1 0 06:17 ? 00:00:00 /usr/sbin/crond
root 5981 1 0 06:17 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe
root 5983 11 0 06:17 ? 00:00:00 [loop0]
root 5984 1 0 06:17 tty1 00:00:00 /bin/login --
root 5985 1 0 06:17 tty2 00:00:00 /sbin/agetty 38400 vc/2 linux
root 5986 1 0 06:17 tty3 00:00:00 /sbin/agetty 38400 vc/3 linux
root 5987 1 0 06:17 tty4 00:00:00 /sbin/agetty 38400 vc/4 linux
root 5988 1 0 06:17 tty5 00:00:00 /sbin/agetty 38400 vc/5 linux
root 5989 1 0 06:17 tty6 00:00:00 /sbin/agetty 38400 vc/6 linux
mysql 6026 5981 0 06:17 ? 00:00:00 /usr/sbin/mysqld --basedir=/usr
root 6029 1 0 06:17 ? 00:00:00 /usr/sbin/cupsd
postgres 6033 1 0 06:17 ? 00:00:00 /usr/bin/postmaster -D /var/lib/
root 6046 1 0 06:17 ? 00:00:00 /usr/bin/mpd /etc/mpd.conf
root 6048 6046 0 06:17 ? 00:00:04 /usr/bin/mpd /etc/mpd.conf
root 6049 6048 0 06:17 ? 00:00:01 /usr/bin/mpd /etc/mpd.conf
root 6054 1 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start
postgres 6059 6033 0 06:17 ? 00:00:00 postgres: writer process
postgres 6060 6033 0 06:17 ? 00:00:00 postgres: stats buffer process
postgres 6061 6060 0 06:17 ? 00:00:00 postgres: stats collector proces
nobody 6062 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start
nobody 6063 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start
nobody 6064 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start
nobody 6065 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start
nobody 6066 6054 0 06:17 ? 00:00:00 /usr/sbin/httpd -k start
nn 6067 5984 0 06:28 tty1 00:00:00 -bash
nn 6071 6067 0 06:28 tty1 00:00:00 xinit
root 6072 6071 1 06:28 tty7 00:04:28 X :0
root 6090 6072 0 06:28 tty7 00:00:00 X :0
nn 6092 6071 0 06:28 tty1 00:00:00 /bin/sh /opt/kde/bin/startkde
nn 6113 1 0 06:28 ? 00:00:00 /usr/bin/gpg-agent --daemon
nn 6116 1 0 06:28 ? 00:00:00 /usr/bin/ssh-agent -s
root 6132 1 0 06:28 tty1 00:00:00 start_kdeinit --new-startup +kcm
nn 6133 1 0 06:28 ? 00:00:00 kdeinit Running...
nn 6136 1 0 06:28 ? 00:00:00 dcopserver [kdeinit] --nosid
nn 6138 6133 0 06:28 ? 00:00:00 klauncher [kdeinit] --new-startu
nn 6140 1 0 06:28 ? 00:00:21 kded [kdeinit] --new-startup
nn 6145 6092 0 06:28 tty1 00:00:00 kwrapper ksmserver
nn 6147 1 0 06:28 ? 00:00:00 ksmserver [kdeinit]
nn 6148 6133 0 06:28 ? 00:00:05 kwin [kdeinit] -session 106e6e64
nn 6150 1 0 06:28 ? 00:00:00 knotify [kdeinit]
nn 6152 1 0 06:28 ? 00:00:02 kdesktop [kdeinit]
nn 6154 1 0 06:28 ? 00:00:11 kicker [kdeinit]
nn 6160 1 0 06:28 ? 00:00:00 kaccess [kdeinit]
nn 6163 1 0 06:28 ? 00:00:00 kmix [kdeinit] -session 106e6e64
nn 6164 6133 0 06:28 ? 00:00:00 konqueror [kdeinit] --preload
nn 6166 6133 0 06:28 ? 00:00:00 konqueror [kdeinit] --preload
nn 6242 6133 0 06:29 ? 00:00:04 /usr/bin/python /usr/bin/sonata
nn 6251 6133 0 09:24 ? 00:00:00 kio_file [kdeinit] file /tmp/kso
nn 6256 1 0 06:29 ? 00:00:00 dbus-launch --autolaunch 27b9194
nn 6257 1 0 06:29 ? 00:00:00 /usr/bin/dbus-daemon --fork --pr
nn 8952 1 0 06:43 ? 00:00:40 kopete
nn 10460 6133 0 12:43 ? 00:00:01 konsole [kdeinit]
nn 10461 10460 0 12:43 pts/2 00:00:00 /bin/bash
nn 11454 10461 0 12:49 pts/2 00:00:00 ps -ef
nn 11590 6133 0 06:58 ? 00:00:03 konsole [kdeinit]
nn 11591 11590 0 06:58 pts/1 00:00:00 /bin/bash
nn 13609 6133 0 07:07 ? 00:00:00 /bin/sh /usr/bin/firefox
nn 13620 13609 0 07:07 ? 00:00:00 /bin/sh /opt/firefox/run-mozilla
nn 13625 13620 1 07:07 ? 00:05:52 /opt/firefox/firefox-bin
nn 13632 1 0 07:07 ? 00:00:00 /opt/gnome/libexec/gconfd-2 12
nobody 24957 6054 0 08:09 ? 00:00:00 /usr/sbin/httpd -k start
Надо отметить, что процесс init всегда имеет идентификатор 1 и PPID равный 0. Хотя в реальности процесса с идентификатором 0 не существует. Дерево процессов можно также пресставить в наглядном виде при помощи опции --forest программы ps:
$ ps -e --forest
PID TTY TIME CMD
1 ? 00:00:01 init
2 ? 00:00:00 migration/0
3 ? 00:00:00 ksoftirqd/0
4 ? 00:00:00 watchdog/0
5 ? 00:00:00 migration/1
6 ? 00:00:00 ksoftirqd/1
7 ? 00:00:00 watchdog/1
8 ? 00:00:00 events/0
9 ? 00:00:00 events/1
10 ? 00:00:00 khelper
11 ? 00:00:00 kthread
35 ? 00:00:00 \_ kblockd/0
36 ? 00:00:00 \_ kblockd/1
37 ? 00:00:00 \_ kacpid
216 ? 00:00:00 \_ kseriod
244 ? 00:00:00 \_ pdflush
245 ? 00:00:00 \_ pdflush
246 ? 00:00:00 \_ kswapd0
247 ? 00:00:00 \_ aio/0
248 ? 00:00:00 \_ aio/1
395 ? 00:00:00 \_ ata/0
396 ? 00:00:00 \_ ata/1
397 ? 00:00:00 \_ ata_aux
407 ? 00:00:00 \_ scsi_eh_0
408 ? 00:00:00 \_ scsi_eh_1
409 ? 00:00:00 \_ scsi_eh_2
410 ? 00:00:00 \_ scsi_eh_3
422 ? 00:00:00 \_ scsi_eh_4
423 ? 00:00:00 \_ scsi_eh_5
1406 ? 00:00:00 \_ kjournald
1443 ? 00:00:00 \_ ksuspend_usbd
1446 ? 00:00:00 \_ khubd
3230 ? 00:00:00 \_ kpsmoused
4652 ? 00:00:00 \_ khpsbpkt
4785 ? 00:00:00 \_ pccardd
4786 ? 00:00:00 \_ tifm/0
4840 ? 00:00:00 \_ knodemgrd_0
4849 ? 00:00:00 \_ kmmcd
5807 ? 00:00:00 \_ kjournald
5810 ? 00:00:00 \_ kjournald
5983 ? 00:00:00 \_ loop0
1462 ? 00:00:00 udevd
5970 ? 00:00:00 syslog-ng
5973 ? 00:00:00 crond
5981 ? 00:00:00 mysqld_safe
6026 ? 00:00:00 \_ mysqld
5984 tty1 00:00:00 login
6067 tty1 00:00:00 \_ bash
6071 tty1 00:00:00 \_ xinit
6072 tty7 00:04:40 \_ X
6090 tty7 00:00:00 | \_ X
6092 tty1 00:00:00 \_ startkde
6145 tty1 00:00:00 \_ kwrapper
5985 tty2 00:00:00 agetty
5986 tty3 00:00:00 agetty
5987 tty4 00:00:00 agetty
5988 tty5 00:00:00 agetty
5989 tty6 00:00:00 agetty
6029 ? 00:00:00 cupsd
6033 ? 00:00:00 postmaster
6059 ? 00:00:00 \_ postmaster
6060 ? 00:00:00 \_ postmaster
6061 ? 00:00:00 \_ postmaster
6046 ? 00:00:00 mpd
6048 ? 00:00:04 \_ mpd
6049 ? 00:00:01 \_ mpd
6054 ? 00:00:00 httpd
6062 ? 00:00:00 \_ httpd
6063 ? 00:00:00 \_ httpd
6064 ? 00:00:00 \_ httpd
6065 ? 00:00:00 \_ httpd
6066 ? 00:00:00 \_ httpd
24957 ? 00:00:00 \_ httpd
4563 ? 00:00:00 \_ httpd
6113 ? 00:00:00 gpg-agent
6116 ? 00:00:00 ssh-agent
6132 tty1 00:00:00 start_kdeinit
6133 ? 00:00:00 kdeinit
6138 ? 00:00:00 \_ klauncher
6148 ? 00:00:05 \_ kwin
6164 ? 00:00:00 \_ konqueror
6166 ? 00:00:00 \_ konqueror
6242 ? 00:00:04 \_ sonata
11590 ? 00:00:03 \_ konsole
11591 pts/1 00:00:00 | \_ bash
3498 pts/1 00:00:09 | \_ kate
3984 pts/1 00:00:03 | \_ kate
13609 ? 00:00:00 \_ firefox
13620 ? 00:00:00 | \_ run-mozilla.sh
13625 ? 00:05:56 | \_ firefox-bin
6251 ? 00:00:00 \_ kio_file
5504 ? 00:00:00 \_ konsole
5505 pts/0 00:00:00 | \_ bash
10460 ? 00:00:01 \_ konsole
10461 pts/2 00:00:00 \_ bash
12140 pts/2 00:00:00 \_ ps
6136 ? 00:00:00 dcopserver
6140 ? 00:00:21 kded
6147 ? 00:00:00 ksmserver
6150 ? 00:00:00 knotify
6152 ? 00:00:02 kdesktop
6154 ? 00:00:11 kicker
6160 ? 00:00:00 kaccess
6163 ? 00:00:00 kmix
6256 ? 00:00:00 dbus-launch
6257 ? 00:00:00 dbus-daemon
8952 ? 00:00:40 kopete
13632 ? 00:00:00 gconfd-2
4026 ? 00:00:00 kio_uiserver
Если вызвать программу ps без аргументов, то будет выведен список процессов, принадлежащих текущей группе, то есть работающих под текущим терминалом. О том, что такое терминалы и группы процессов, будет рассказано в последующих главах.
Процесс может узнать свой идентификатор (PID), а также родительский идентификатор (PPID) при помощи системных вызовов getpid() и getppid().
Системные вызовы getpid() и getppid() имеют следующие прототипы:
pid_t getpid (void);
pid_t getppid (void);
Для использования getpid() и getppid() в программу должны быть включены директивой #include заголовочные файлы unistd.h и sys/types.h (для типа pid_t). Вызов getpid() возвращает идентификатор текущего процесса (PID), а getppid() возвращает идентификатор родителя (PPID). pid_t - это целый тип, размерность которого зависит от конкретной системы. Значениями этого типа можно оперировать как обычными целыми числами типа int.
Рассмотрим теперь простую программу, которая выводит на экран PID и PPID, а
затем "замирает" до тех пор, пока пользователь не нажмет <Enter>.
/* getpid.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main (void)
{
pid_t pid, ppid;
pid = getpid ();
ppid = getppid ();
printf ("PID: %d\n", pid);
printf ("PPID: %d\n", ppid);
fprintf (stderr, "Press <Enter> to exit...");
getchar ();
return 0;
}
Проверим теперь, как работает эта программа. Для этого откомпилируем и запустим
ее:
$ gcc -o getpid getpid.c
$ ./getpid
PID: 27784
PPID: 6814
Press <Enter> to exit...
Теперь, не нажимая <Enter>, откроем другое терминальное окно и проверим,
правильность работы системных вызовов getpid() и getppid():
$ ps -ef | grep getpid
nn 27784 6814 0 01:05 pts/0 00:00:00 ./getpid
nn 28249 28212 0 01:07 pts/1 00:00:00 grep getpid
Как уже говорилось ранее, процесс в Linux - это нечто, выполняющее программный код. Этот код называют образом процесса (process image). Рассмотрим простой пример, когда вы находитесь в оболочке bash и выполняете команду ls. В этом случае происходит следующее. Образ программы-оболочки bash выполняется в процессе #1. Затем вы вводите команду ls, и оболочка определяет, что нужно запустить внешнюю программу (/bin/ls). Тогда процесс #1 создает свою почти точную копию, процесс #2, который выполняет тот же самый программный код. После этого процесс #2 заменяет свой текущий образ (оболочку) другим образом (программой /bin/ls). В итоге получаем отдельный процесс, выполняющий отдельную программу.
"К чему такая путаница?" - спросите вы. Зачем сначала "клонировать" процесс, а затем заменять в нем образ? Не проще ли все делать одной-единственной операцией? Ответы на подобные вопросы дать тяжело, но, как правило, с опытом прихоидит понимание того, что подобная схема является одной из граней красоты Unix-систем.
Попробуем все-таки разобраться, почему в Unix-системах порождение процесса отделено от запуска программы. Для этого выясним, что же происходит с данными при "клонировании" процесса. Итак, каждый процесс хранит в своей памяти различные данные (переменные, файловые дескрипторы и проч.). При порождении нового процесса, потомок получает точную копию данных родителя. Но как только новый процесс создан, родитель и потомок уже распоряжаются своими копиями по своему усмотрению. Это позволяет распараллелить программу, заставив ее выполнять какой-нибудь трудоемкий алгоритм в отдельном процессе.
Может быть кто-то из вас слышал про то, что в Linux есть потоки, которые позволяют в одной программе реализовывать параллельное выполнение нескольких функций. Опять же возникает вопрос: "Если есть потоки, зачем вся эта головомойка с клонированиями и заменой образов?". А дело в том, что потоки работают с общими данными и выполняются в одной программе. Если в потоке произошло что-то страшное, то это, как правило, отражается на всей программе в целом. Хотя технически потоки реализованы в Linux на базе процессов, но процесс все же является более независимой единицей. Крах дочернего процесса никак не отражается на работе родителя, если сам родитель этого не пожелает.
По правде сказать, программисты редко прибегают к методике распараллеливания одной программы при помощи процессов. Но суть в том, что в Unix-системах программист обладает полной свободой выбора стратегии многозадачности. И это здорово!
Разберемся теперь с тем, как на практике происходит "клонирование" процессов. Для этого используется простой системный вызов fork(), прототип которого находится в файле unistd.h:
pid_t fork (void);
Если fork() завершается с ошибкой, то возвращается -1. Это редкий случай, связанный с нехваткой памяти или превышением лимита на количество процессов. Но если разделение произошло, то программе нужно позаботиться об идентификации своего "Я", то есть определении того, где родитель, а где потомок. Это делается очень просто: в родительский процесс fork() возвращает идентификатор потомка, а потомок получает 0. Следующий пример демонстрирует то, как это происходит.
/* fork01.c */
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main (void)
{
pid_t pid = fork ();
if (pid == 0) {
printf ("child (pid=%d)\n", getpid());
} else {
printf ("parent (pid=%d, child's pid=%d)\n", getpid(), pid);
}
return 0;
}
Проверяем, что получилось:
$ gcc -o fork01 fork01.c
$ ./fork01
child (pid=21026)
parent (pid=21025, child's pid=21026)
Обратите внимание, что поскольку после вызова fork() программу выполняли уже два независимых процесса, то сообщение родителя вполне могло бы появиться первым.
Итак, теперь мы умеем порождать процессы. Научимся теперь заменять образ текущего процесса другой программой. Для этих целей используется системный вызов execve(), который объявлен в заголовочном файле unistd.h вот так:
int execve (const char * path, char const * argv[], char * const envp[]);
Все очень просто: системный вызов execve() заменяет текущий образ процесса программой из файла с именем path, набором аргументов argv и окружением envp. Здесь следует только учитывать, что path - это не просто имя программы, а путь к ней. Иными словами, чтобы запустить ls, нужно в первом аргументе указать "/bin/ls".
Массивы строк argv и envp обязательно должны заканчиваться элементом NULL. Кроме того, следует помнить, что первый элемент массива argv (argv[0]) - это имя программы или что-либо иное. Непосредственные аргументы программы отсчитываются от элемента с номером 1.
В случае успешного завершения execve() ничего не возвращает, поскольку новая программа получает полное и безвозвратное управление текущим процессом. Если произошла ошибка, то по традиции возвращается -1.
Рассмотрим теперь пример программы, которая заменяет свой образ другой программой.
/* execve01.c */
#include <unistd.h>
#include <stdio.h>
int main (void)
{
printf ("pid=%d\n", getpid ());
execve ("/bin/cat", NULL, NULL);
return 0;
}
Итак, данная программа выводит свой PID и передает безвозвратное управление программе cat без аргументов и без окружения. Проверяем:
$ gcc -o execve01 execve01.c
$ ./execve01
pid=30150
Программа вывела идентификатор процесса и замерла в смиренном ожидании. Откроем теперь другое терминальное окно и проверим, что же творится с нашим процессом:
$ ps -e | grep 30150
30150 pts/3 00:00:00 cat
Итак, мы убедились, что теперь процесс 30150 выполняет программа cat. Теперь можно вернуться в исходное окно и нажатием Ctrl+D завершить работу cat.
И, наконец, следующий пример демонстрирует запуск программы в отдельном процессе.
/* forkexec01.c */
#include <unistd.h>
#include <stdio.h>
extern char ** environ;
int main (void)
{
char * echo_args[] = { "echo", "child", NULL };
if (!fork ()) {
execve ("/bin/echo", echo_args, environ);
fprintf (stderr, "an error occured\n");
return 1;
}
printf ("parent");
return 0;
}
Проверяем:
$ gcc -o forkexec01 forkexec01.c
$ ./forkexec01
parent
child
Обратите внимание, что поскольку execve() не может возвращать ничего кроме -1, то для обработки возможной ошибки вовсе не обязательно создавать ветвление. Иными словами, если вызов execve() возвратил что-то, то это однозначно ошибка.