Правила применения setpgid() несколько сложны.
1. Процесс может устанавливать группу для себя или одного из своих потомков. Он не может изменять группу для любого другого процесса в системе, даже если процесс, вызвавший setpgid(), имеет административные полномочия.
2. Лидер сеанса не может изменить свою группу.
3. Процесс не может быть перемещен в группу, чей лидер представляет другой сеанс, чем он сам. Другими словами, все процессы в группе должны относиться к одному и тому же сеансу.
Вызов setpgid() помещает вызывающий процесс в свою собственную группу и собственный сеанс. Это необходимо для того, чтобы гарантировать, что два сеанса не содержат процессы, принадлежащие к одной и той же группе.
Полный пример групп процессов будет приведен при обсуждении системы управления заданиями в главе 15.
Когда соединение с терминалом теряется, ядро посылает сигнал (SIGHUP; подробнее о сигналах рассказывается в главе 12) лидеру сеанса, содержащему группу процессов переднего плана данного терминала. Обычно это командная оболочка. Это позволит оболочке безусловно прерывать пользовательские процессы, извещая их о том, что пользователь выходит из системы (обычно посредством SIGHUP), либо выполнить некоторые другие действия (или бездействие). Хотя это все может показаться усложненным, это дает возможность лидеру группы сеанса принимать решения о том, как управлять закрывающимися терминалами, вместо того, чтобы возлагать эту обязанность на ядро. Это также дает возможность администраторам гибко управлять политиками пользовательских учетных записей.
Определение группы процесса может быть выполнено просто, с помощью функций getpgid() и getpgrp().
pid_t getpgid(pid_t pid) Возвращает pgid процесса pid. Если pid равен 0, возвращается pgid текущего процесса. Для вызова не требуется никаких специальных полномочий. Любой процесс может определять группу, к которой принадлежит любой другой процесс. pid_t getpgrp(void) Возвращает pgid текущего процесса pid (эквивалентно getprgid(0))
10.6.4. Висячие группы процессов
Механизм прерывания процессов (либо возобновления их работы после приостановки) при исчезновении их сеанса довольно сложен. Представьте себе сеанс со многими группами процессов в нем (см. рис. 10.1). Сеанс запущен на терминале, и обычная системная оболочка является его лидером.
Когда лидер сеанса (оболочка) завершается, ее группы процессов оказываются в сложной ситуации. Если они активно работают, то лишаются возможности использовать стандартные потоки stdin и stdout, поскольку терминал закрыт. Если они приостановлены, то вероятно, никогда не будут запущены снова, поскольку пользователь терминала не имеет возможности перезапустить их, к тому же то, что они не могут быть запущены, означает также, что они не могут быть и прерваны.
В этой ситуации каждая такая группа процессов получает название висячей (orphaned). Стандарт POSIX определяет ее как группу процессов, чей родитель является также членом этой группы либо не является членом сеанса этой группы. Другими словами, группа процессов не является висячей до тех пор, пока у нее есть родительский процесс, принадлежащий тому же сеансу, но другой группе.
Хотя оба определения выглядят сложными, концепция достаточно проста. Если группа процессов приостановлена, и не существует процесса, который бы принудил ее возобновиться, то эта группа становится висячей[33].
Когда завершает работу командная оболочка, все ее дочерние процессы становятся дочерними по отношению к процессу init, оставаясь при этом в своих исходных сеансах. Предполагая, что все программы в сеансе являются потомками оболочки, все группы процессов этого сеанса становятся висячими[34]. Когда группа процессов превращается в висячую, каждый процесс этой группы получает сигнал SIGHUP, что обычно прерывает программу.
Программы, которые не прерываются по сигналу SIGHUP, получают сигнал SIGCONT, который продолжает выполнение приостановленных процессов. Такая последовательность прерывает большинство процессов и обеспечивает оставшимся возможность работать (то есть гарантирует, что они не будет в приостановленном состоянии)[35].
Как только процесс становится висячим, он принудительно отключается от своего управляющего терминала (позволяя новому пользователю при необходимости применять этот терминал). Если продолжающие работать программы пытаются получить доступ к терминалу, эти попытки вызывают ошибки, устанавливающие errno в значение EIO. Процессы остаются в том же сеансе, и идентификатор сеанса не используется для новых идентификаторов процессов до тех пор, пока не завершатся все процессы данного сеанса.
Чтобы помочь проиллюстрировать идеи, обсуждаемые в нашей книге, на протяжении последующих разделов книги мы разработаем подмножество командной оболочки Unix. В конечном итоге наша оболочка будет поддерживать следующее.
• Простые встроенные команды.
• Запуск внешних команд.
• Перенаправление ввода-вывода (>, | и так далее).
• Управление заданиями.
Полный исходный текст окончательной версии этой оболочки, ladsh4.с, представлен в приложении Б. По мере добавления в ladsh новых средств, изменения исходного текста описываются в тексте книги. Чтобы уменьшить количество изменений, которые мы вносим между версиями, некоторые ранние версии несколько более сложны, чем было бы нужно. Эти небольшие усложнения, однако, далее в книге упрощают разработку оболочки, поэтому будьте терпеливы. Просто пока поверьте, что эти фрагменты кода необходимы; все они будут объяснены позднее.
10.7.1. Запуск внешних программ с помощью ladsh
Вот первая (и самая простая) версия ladsh, называемая ladsh1.
1: /*ladsh1.c*/
2:
3: #include <ctype.h>
4: #include <errno.h>
5: #include <fcntl.h>
6: #include <signal.h>
7: #include <stdio.h>
8: #include <stdlib.h>
9: #include <string.h>
10: #include <sys/ioctl.h>
11: #include <sys/wait.h>
12: #include <unistd.h>
13:
14: #define MAX_COMMAND_LEN 250 /* максимальная длина отдельной
15: командной строки */
16: #define JOB_STATUS_FORMAT "[%d]%-22s%.40sn"
17:
18: struct jobSet {
19: struct job *head; /* заголовок списка запущенных заданий */
20: struct job *fg; /* текущее задание переднего плана */
21: };
22:
23: struct childProgram {
24: pid_t Pid; /* 0 на выходе */
25: char **argv; /* имя программы с аргументами */
26: };
27:
28: struct job {
29: int job Id; /* номер задания */
30: int numProgs; /* общее кол-во программ в задании */
31: int runningProgs; /* кол-во работающих программ */
32: char *text; /* имя задания */
33: char *cmdBuf; /* буфер различных argv */
34: pid_t pgrp; /* идентификатор группы процессов задания */
35: struct childProgram *progs; /* массив программ в задании */
36: struct job *next; /* для слежения за фоновыми программами */
37: };
38:
39: void freeJob(struct job *cmd) {
40: int i;
41:
42: for (i=0; i<cmd->numProgs; i++) {
43: free (cmd->progs[i].argv);
44: }
45: free(cmd->progs);
46: if (cmd->text) free(cmd->text);
47: free(cmd->cmdBuf);
48: }
49:
50: int getCommand(FILE *source, char *command) {
51: if (source == stdin) {
52: printf("#");
53: fflush(stdout);
54: }
55:
56: if (!fgets(command, MAX_COMMAND_LEN, source)) {
57: if (source==stdin) printf("n");
58: return 1;
59: }
60:
61: /* удалить завершающий перевод строки */
62: command[strlen(command) - 1] = ' ';
63:
64: return 0;
65: }
66:
67: /* Возвратить cmd->numProgs как 0, если нет никаких команд (то есть пустая
68: строка). Если найдена правильная команда, commandPtr устанавливается в
69: указатель на начало следующей команды (если исходная команда имеет более
70: одного задания, ассоциированного с ней) или NULL, если