Эта глава открывает большую и очень важную для 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() возвратил что-то, то это однозначно ошибка.