Выбрать главу

11.7.2. Изменения в коде

Как только в parseCommand() будут правильно отражены структуры данных, то запуск команд в правильном порядке становится довольно простым при достаточном внимании к деталям. Прежде всего, мы добавляем цикл в parseCommand() для запуска дочерних процессов, поскольку теперь их может быть множество. Прежде чем войти в цикл, мы устанавливаем nextin и nextout, которые являются файловыми дескрипторами, используемыми в качестве стандартных потоков ввод и вывода для следующего запускаемого процесса. Для начала мы используем те же stdin и stdout, что и оболочка.

Теперь посмотрим, что происходит внутри цикла. Основная идея описана ниже.

1. Если это финальный процесс в задании, убедиться, что nextout указывает на stdout. В противном случае нужно подключить вывод этого задания к входному концу неименованного канала.

2. Породить новый процесс. Внутри дочернего перенаправить stdin и stdout, как указано с помощью nextin, nextout и всех специфицированных ранее перенаправлений.

3. Вернувшись обратно в родительский процесс, закрыть nextin и nextout, используемые только что запущенным дочерним процессом (если только они не являются потоками ввода и вывода самой оболочки).

4. Теперь настроить следующий процесс в задании для приема его ввода из вывода процесса, который мы только что создали (через nextin).

Вот как эти идеи перевести на С.

365: nextin=0, nextout=1;

366: for (i=0; i<newJob.numProgs; i++) {

367:  if ((i+1) < newJob.numProgs) {

368:   pipe(pipefds);

369:   nextout = pipefds[1];

370:   } else {

371:    nextout = 1;

372:   }

373:

374:   if (!(newJob.progs[i].pid = fork())) {

375:    if (nextin != 0) {

376:     dup2(nextin, 0);

377:     close(nextin);

378:    }

379:

380:    if (nextout != 1) {

381:     dup2(nextout, 1);

382:     close(nextout);

383:    }

384:

385:    /* явное перенаправление перекрывает каналы */

386:    setupRedirections(newJob.progs+i);

387:

388:    execvp(newJob.progs[i].argv[0], newJob.progs[i].argv);

389:    fprintf(stderr, "exec() of %s failed: %s\n",

390:     newJob.progs[i].argv[0],

391:    strerror(errno));

392:    exit(1);

393:   }

394:

395:   /* поместить наш дочерний процесс в группу процессов,

396:      чей лидер - первый процесс канала */

397:   setpgid(newJob.progs[i].pid, newJob.progs[0].pid);

398:

399:   if (nextin != 0) close(nextin);

400:   if (nextout != 1) close (nextout);

401:

402:   /* Если больше нет процессов, то nextin - мусор,

403:      но это не имеет значения */

404:   nextin = pipefds[0];

Единственный код, добавленный в ladsh2.с для обеспечения перенаправлений — это функция setupRedirections(), код которой останется неизменным во всех последующих версиях ladsh. Ее задача состоит в обработке спецификаторов struct redirectionSpecifier для дочерних заданий и соответствующей модификации дочерних файловых дескрипторов. Мы рекомендуем просмотреть код этой функции в приложении Б.

Глава 12

Обработка сигналов

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

Сигналы всегда были неотъемлемой частью Unix. Ядро использует их для извещения процессов о разнообразных событиях, включая перечисленные ниже.

• Уничтожение одного из дочерних процессов.

• Установка предупреждений устаревшим процессам.

• Изменение размеров окна терминала.

Эти сообщения имеют важное свойство: все они асинхронные. Процесс не имеет никакого контроля над тем, когда завершится один из его дочерних процессов — это может случиться в любой точке выполнения родительского процесса. Каждое из этих событий посылает сигнал процессу. При получении сигнала процесс может сделать одну из трех вещей.

• Проигнорировать сигнал.

• Позволить ядру запустить специальную часть процесса, прежде чем продолжить выполнение основной его части (это называется перехватом сигнала).

• Позволить ядру выполнить действие по умолчанию, которое зависит от конкретного полученного сигнала.

Концептуально это довольно просто. Однако история развития средств работы с сигналами видна, когда вы сравните различные интерфейсы сигналов, которые поддерживаются различными реализациями Unix. BSD, System V и System 3 поддерживают различные и несовместимые программные интерфейсы сигналов. POSIX определил стандарт, теперь поддерживаемый почти всеми версиями Unix (включая Linux), который был тогда расширен для обработки новой семантики сигнала (вроде формирования очереди сигналов) как части определения сигнала в режиме реального времени POSIX (POSIX Real Time Signal). В этой главе обсуждается исходное выполнение сигналов Unix перед объяснением основ программного интерфейса POSIX и их расширений Real Time Signal, поскольку появление многих возможностей POSIX API было мотивировано недостатками в более ранних реализациях системы сигналов.

12.1. Концепция сигналов

12.1.1. Жизненный цикл сигнала

Сигналы имеют четко определенный жизненный цикл: они создаются, сохраняются до тех пор, пока ядро не выполнит определенное действие на основе сигнала, а затем вызывают совершение этого действия. Создание сигнала называют по-разному: поднятие (raising), генерация или посылка сигнала. Обычно процесс посылает сигнал другому процессу, в то время как ядро генерирует сигналы для отправки процессу. Когда процесс посылает сигнал самому себе, говорят, что он поднимает его. Однако эти термины используются не особо согласованно.

Между временем, когда сигнал отправлен и тем, когда он вызывает какое-то действие, его называют ожидающим (pending). Это значит, что ядро знает, что сигнал должен быть обработан, но пока не имеет возможности сделать это. Как только сигнал поступает в процесс назначения, он называется доставленным. Если доставленный сигнал вызывает выполнение специального фрагмента кода (имеется в виду обработчик сигнала), то такой сигнал считается перехваченным. Есть разные способы, которыми процесс может предотвратить асинхронную доставку сигнала, но все же обработать его (например, с помощью системного вызова sigwait()). Когда такое случается, сигнал называют принятым.