Программирование PC FAQ программирования Linux: управление процессами Mon, April 29 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

FAQ программирования Linux: управление процессами Печать
Добавил(а) microsin   

Здесь приведен перевод главы 1 из FAQ по программированию Linux [1], посвященной управлению процессами (Process Control). Описание незнакомых терминов и аббревиатур см. в разделе "Словарик", в конце статьи [3].

Этот FAQ по программированию в Linix (перевод статьи [1]) изначально начал собирать Patrick Horgan в мае 1996. После него Andrew Gierth взял на себя работу по наполнению этого FAQ (см. врезку "Оригинальные источники и авторы"). Материалы ответов на вопросы был реорганизованы, и этот FAQ все еще можно считать "находящимся в разработке".

Веб-версия оригинала находится по ссылке http://www.erlenstar.demon.co.uk/unix/faq_toc.html, а также на зеркале http://www.whitefang.com/unix/faq_toc.html.

Также этот документ доступен на FTP архивов news.answers на сервере rtfm.mit.edu, и на множестве сайтов по всему миру. Имя официального архива 'unix-faq/programmer/faq'. Сайты, которые архивируют посты *.answers по группам, также должны нести этот файл в директории 'comp.unix.programmer'.

Все поставляемые материалы редактируются их авторами, которые ответственны за любые свои ошибки или упущения.

Copyright (C) 1997, 1998, 1999, 2000 Andrew Gierth. This document may be distributed freely on Usenet or by email; it may be archived on FTP or WWW sites that mirror the news.answers archives, provided that all reasonable efforts are made to ensure that the archive is kept up-to-date. (This permission may be withdrawn on an individual basis.)  It may not be published in any other form, whether in print, on the WWW, on CD-ROM, or in any other medium, without the express permission of the maintainer.

Список поставщиков материалов, без алфавитного порядка:

Andrew Gierth       < andrew@erlenstar.demon.co.uk>
Patrick J. Horgan   withheld

Stephen Baynes      < stephen.baynes@soton.sc.philips.com>
James Raynard       withheld
Michael F. Quigley  withheld
Ken Pizzini         withheld
Thamer Al-Herbish   withheld
Nick Kew            < nick.kew@pobox.com>
Dan Abarbanel       withheld
Billy Chambless     < billy@cast.msstate.edu>
Walter Briscoe      < walter@wbriscoe.demon.co.uk>
Jim Buchanan        < jbuchana@buchanan1.net>
Dave Plonka         < plonka@doit.wisc.edu>
Daniel Stenberg     withheld
Ralph Corderoy      < ralph@inputplus.demon.co.uk>
Stuart Kemp         withheld
Sergei Chernev      < ser@nsu.ru>
Bjorn Reese         withheld
Joe Halpin          < jhalpin@nortel.ca>
Aaron Crane         < aaronc@pobox.com>
Geoff Clare         < gwc@root.co.uk>

[Список вопросов]

1. Process Control (управление процессами)

  1.1 Создание новых процессов: fork()

    1.1.1 Что делает fork()?
    1.1.2 В чем разница между fork() и vfork()?
    1.1.3 Почему используют _exit вместо exit в дочерней ветви fork?

  1.2 Environment variables (переменные окружения)

    1.2.1 Как можно получить/установить переменную окружения из программы?
    1.2.2 Как полностью прочитать окружение?

  1.3 Как можно приостановить выполнение (sleep) на интервал меньше секунды?

  1.4 Как получить более точную версию alarm()?

  1.5 Как могут обмениваться данными друг с другом родительский и дочерний процессы?

  1.6 Как избавиться от зомби-процессов?

    1.6.1 Что такое зомби?

    1.6.2 Как предотвратить их возникновение?

  1.7 Как запустить мою программу в качестве сервиса/демона (daemon)?

  1.8 Как получить информацию о процессе в системе, как это делает утилита ps?

  1.9 Как по идентификатору pid определить работающую программу?

  1.10 Что означает возвращаемое значение system/pclose/waitpid?

  1.11 Как определить используемое процессом количество памяти?

  1.12 Почему процессы никогда не уменьшаются в размере?

  1.13 Как поменять имя моей программы (которое отображается утилитой ps)?

  1.14 Как найти исполняемый файл процесса?

    1.14.1 Куда поместить конфигурационную информацию программы (конфигурационные файлы)?

  1.15 Почему мой процесс не получает SIGHUP, когда его родитель умирает?

  1.16 Как прибить всех потомков процесса?

1.1 Создание новых процессов: fork()
====================================

1.1.1 Что делает fork()?
------------------------

   #include < sys/types.h>
   #include < unistd.h>
pid_t fork(void);

Функция fork() используется для создания нового процесса из существующего процесса. Новый процесс называется дочерним процессом (child process), а существующий процесс, из которого новый (дочерний) процесс создается, называется родительским процессом (parent process). Вы можете проверить, что есть что, путем анализа возвращаемого из функции fork() значения. Родителю будет возвращен идентификатор (pid) дочернего процесса, но дочернему процессу будет возвращено значение 0. Ниже приведен простой код, демонстрирующий базовое использование функции fork().

   pid_t pid;
   
   switch (pid = fork())
   {
   case -1:
      /* Значение -1 показывает неудачу вызова fork.
         Возможные причины такой ситуации:
         - закончились слоты процессов;
         - закончилась виртуальная память. */
      perror("The fork failed!");
      break;
   
   case 0:
      /* Нулевое значение pid показывает, что это дочерний процесс. */
      /* Тут могут быть какие-то действия, соответствующие child-процессу. */
      /* ... */
      /* Однако после этого мы должны сделать что-то типа: */
      _exit(0);
   
   default:
      /* pid > 0 означает, что это родительский процесс, и мы получили
         pid дочернего процесса. */
      printf("Child's pid is %d\n",pid);
   }

Конечно, вместо switch() можно также использовать конструкции if()... else..., однако показанный выше код показывает идею в полезной и понятной форме. Такой код даст информацию, что унаследовано, а что нет у дочернего процесса. Этот список может меняться в зависимости от реализации Unix, что следует иметь в виду. Обратите внимание, что дочерний процесс получает КОПИИ этих вещей, но не сами эти реальные вещи.

Дочерний процесс наследует от родителя:

   * учетные данные (process credentials, (real/effective/saved UID-ы и GID-ы)
   * окружение (environment)
   * стек
   * память
   * дескрипторы открытых файлов (обратите внимание, что нижележащие позиции файлов совместно используются дочерним и родительским процессами, что может привести к путанице)
   * флаги close-on-exec
   * настройки обработки сигналов (signal handling settings)
   * nice value
   * класс планировщика (scheduler class)
   * идентификатор группы процесса (process group ID)
   * идентификатор сессии (session ID)
   * текущая рабочая директория (current working directory)
   * корневая директория (root directory)
   * маска режима создания файла (umask)
   * ограничения по ресурсам
   * управляющий терминал

Уникальны для дочернего процесса:

   * идентификатор процесса (process ID)
   * другой идентификатор родительского процесса (parent process ID)
   * собственная копия дескрипторов файлов и потоков директорий
   * блокировки process, text, data и другие блокировки памяти не наследуются
   * интервалы времени процесса (process times) в структуре tms
   * инициализации ресурсов устанавливаются в 0
   * сигналы ожидания (pending signals) инициализируются в пустой набор (empty set)
   * не наследуются таймеры, созданные через timer_create
   * операции асинхронного ввода или вывода не наследуются

1.1.2 Чем отличаются fork() и vfork()?
--------------------------------------

На некоторых системах есть вызовы vfork(), которые были изначально разработаны как облеченная (менее затратная по ресурсам) версия fork(). Поскольку fork() вовлекает копирование всего адресного пространства процесса, и поэтому была довольно затратна по ресурсам, была добавлена функция vfork() (в 3.0BSD).

Однако с момента введения vfork() реализация fork() получила значительные улучшения, особенно добавлением функции copy-on-write, когда копирование адресного пространства прозрачно подделывается, позволяя обоим процессам ссылаться на одну и ту же физическую память до тех пор, пока одна из сторон не изменит её. Это в значительной степени устраняет обоснованность использования vfork(); действительно, большая часть систем в настоящее время не имеет полного первоначального функционала vfork(). Хотя для совместимости все еще может присутствовать вызов vfork(), который просто вызывает fork(), не пытаясь эмулировать семантику vfork().

В результате очень нежелательно эксплуатировать любые отличия между fork() и vfork(). В действительности скорее всего вообще не стоит использовать vfork() за исключением ситуаций, когда вы точно знаете, почему так делаете.

Основное отличие между этими двумя функциями в том, что когда новый процесс создан через vfork(), родительский процесс временно приостанавливается, и дочерний процесс может заимствовать адресное пространство родителя. Это странное состояние продолжается до тех пор, пока либо дочерний процесс выполнит выход, или вызовет execve(), после чего родительский процесс продолжает работу.

Это означает, что дочерний процесс, созданный vfork(), должен тщательно избегать модификации переменных родительского процесса. В частности, дочерний процесс НЕ ДОЛЖЕН делать возврат из функции, содержащей vfork(), и НЕ ДОЛЖЕН вызывать exit() (если нужно выполнить выход, он должен вызвать _exit(); в действительности это верно для дочернего процесса обычного fork().

1.1.3 Почему используется _exit вместо exit в дочерней ветви fork?
------------------------------------------------------------------

Есть несколько отличий между exit() и _exit(), которые становятся важны при использовании fork(), и особенно для использования vfork().

Основное отличие между функциями exit() и _exit() в том, что первая выполняет очистку (clean-up), относящуюся к конструкциям режима пользователя (user-mode constructs) в библиотеке, и вызывает предоставленные пользователем функции очистки, в то время как вторая (_exit()) выполняет только ядерную очистку процесса (kernel cleanup).

В дочерней ветви вызова fork() обычно некорректно использовать exit(), потому что это может привести к тому, что буферы stdio будут освобождены дважды, и внешние файлы неожиданно удаляются. В коде C++ ситуация еще хуже, потому что деструкторы для статических объектов могут работать некорректно. Замечание: существуют некоторые необычные случаи, такие как демоны, когда родительский процесс должен вызвать _exit() вместо дочернего процесса; основное правило, применимое в подавляющем большинстве случаев, заключается в том, что exit() следует вызывать для каждого входа в 'main'.

В дочерней ветви vfork() использование exit() даже более опасно, поскольку будет влиять на состояние родительского процесса.

1.2 Переменные окружения (environment variables)
================================================

1.2.1 Как можно получить/установить переменную окружения из программы?
----------------------------------------------------------------------

Получение значения переменной окружения делается можно сделать с помощью getenv().

   #include < stdlib.h>
     
   char *getenv(const char *name);

Установка значения переменной окружения делается с помощью putenv().

   #include < stdlib.h>
     
   int putenv(char *string);

Строка, переданная в putenv, НЕ ДОЛЖНА быть освобождена или сделана недостоверной, поскольку указатель на неё хранится putenv(). Это значит, они должны быть в статическом буфере, или должны быть выделены из кучи. Строка может быть освобождена, если переменная окружения переназначена или удалена другим вызовом putenv().

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

Предположим, что вы хотите получить значение переменной окружения TERM. Для этого нужно использовать следующий код:

   char *envvar;
   
   envvar=getenv("TERM");
   
   printf("The value for the environment variable TERM is ");
   if(envvar)
   {
      printf("%s\n",envvar);
   }
   else
   {
      printf("not set.\n");
   }

Теперь предположим, что вы хотите создать новую переменную окружения MYVAR со значением MYVAL. Вот так это можно сделать:

   static char envbuf[256];
   
   sprintf(envbuf,"MYVAR=%s","MYVAL");
   
   if(putenv(envbuf))
   {
      printf("Sorry, putenv() couldn't find the memory for %s\n",envbuf);
      /* Здесь может быть exit() или что-то другое, что вам нужно */
   }

1.2.2 Как полностью прочитать окружение?
----------------------------------------

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

Глобальная переменная environ содержит в себе массив указателей на строки переменных окружения, где каждая строка имеет форму "NAME=value". Указатель NULL используется в качестве маркера конца этого массива. Ниже показана простейшая программа, печатающая текущее окружение (наподобие утилиты printenv):

   #include < stdio.h>
   
   extern char **environ;
   
   int main()
   {
      char **ep = environ;
      char *p;
      while ((p = *ep++))
         printf("%s\n", p);
      return 0;
   }

В общем переменная окружения также передается в третьем, не обязательном параметре функции main(), т. е. пример выше может быть переписан так:

   #include < stdio.h>
   
   int main(int argc, char **argv, char **envp)
   {
      char *p;
      while ((p = *envp++))
         printf("%s\n", p);
      return 0;
   }

Однако, несмотря на универсальную поддержку, этот метод фактически не определен стандартами POSIX (также этот метод в целом менее полезен).

1.3 Как можно приостановить выполнение (sleep) на интервал меньше секунды?
==========================================================================

Функция sleep(), которая доступа во всех операционных системах семейства Unix, позволяет создавать длительность задержки, указанную в секундах. Если вы захотите получить более точный интервал, то следует обратить внимание на альтернативыs:

   * На многих системах есть функция usleep() (см. [2, 3]).
   * Можно использовать select() или poll(), не указывая дескрипторы файла для проверки; это общая техника написать функцию usleep() на основе одной из этих функций (см. comp.unix.questions FAQ для некоторых примеров).
   * Если в вашей системе есть itimer-ы (что скорее всего), то вы можете сделать свою usleep() с их помощью (см. в исходном коде BSD функцию usleep(), чтобы узнать как это делается).
   * Если у вас есть поддержка POSIX realtime, то доступна функция nanosleep() [3].

Из всего перечисленного select() возможно наиболее портируемый вариант (и стратегически это часто более эффективно, чем usleep(), или метод на основе itimer). Однако поведение может отличаться, если сигналы перехватываются во время формирования задержки; в зависимости от приложения это может создавать проблему.

Какой бы маршрут вы ни выбрали, важно понимать, что вы можете быть ограничены разрешающей способностью таймера системы (некоторые системы позволяют задавать очень короткие интервалы времени, а у других разрешающая способность может быть порядка 10 мс, и все тайминги будут округляться до этого кванта времени). Так что, как и в случае sleep(), задержку можно указать только до некоторой гарантированной минимальной величины; после истечения указанного периода времени будет иметь место неопределенная задержка до момента, когда планировщик решит передать управление вашему процессу в следующий раз.

1.4 Как получить более точную версию alarm()?
=============================================

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

Таймеры itimer могут использоваться для реализации однократных или повторяющихся сигналов; также в основном доступны 3 отдельных таймера:

ITIMER_REAL
     подсчитывает реальное (wall clock) время, и посылает сигнал SIGALRM.

ITIMER_VIRTUAL
     подсчитывает виртуальное (пользовательское CPU) время, и посылает сигнал SIGVTALRM.

ITIMER_PROF
     подсчитывает пользовательское и системное время CPU, и посылает сигнал SIGPROF; он предназначен для использования переводчиками для профилирования.

Однако таймеры itimer не являются частью многих стандартов несмотря на то, что он присутствует начиная с 4.2BSD. Расширения POSIX realtime определяют некоторые похожие, но другие функции.

1.5 Как могут обмениваться данными друг с другом родительский и дочерний процессы?
==================================================================================

В системе parent и child могут взаимодействовать через любую из схем inter-process communication (pipes, sockets, message queues, shared memory). Однако также есть некоторые специальные способы обмена, использующие взаимосвязь процессов по отношению друг к другу как родительский и дочерний.

Один из самых очевидных факторов - родительский процесс может получить статус выхода дочернего процесса.

Поскольку дочерний процесс наследует дескрипторы файлов родителя, родитель может открыть оба конца канала (pipe), выполнить fork, затем родитель закрывает один конец канала, и дочерний элемент закрывает другой конец канала. Это то, что происходит когда вы вызываете popen() для запуска другой программы из вашей, т. е. вы можете записывать в файловый дескриптор, возвращенный из popen(), и дочерний процесс видит его как свой stdin, или вы можете читать из дескриптора файла и посмотреть, что программа записала в свой stdout. Замечание: параметр mode для popen() определяет какой из них; если вы хотите использовать оба, то можете сделать все самостоятельно без особых трудностей.

Кроме того, дочерний процесс наследует сегменты памяти, подключенные анонимно (или путем использования mmap специального файла /dev/zero) родительским процессом; эти сегменты общей памяти недоступны из несвязанных процессов.

1.6 Как избавиться от зомби-процессов?
======================================

1.6.1 Что такое зомби?
----------------------

Когда программа разветвляется (делает вызовы fork), и дочерний процесс завершается до родительского, ядро (kernel) все еще хранит некоторую информацию этого завершенного дочернего процесса для случая, когда родительскому процессу может эта информация понадобиться. Например, родительскому процессу может понадобиться проверить статус выхода (exit status) дочернего процесса. Чтобы получить эту информацию, родитель вызывает wait(); когда это происходит, ядро может отбросить эту информацию.

В этом интервале между событиями, когда дочерний процесс завершается, и когда родитель вызывает wait(), дочерний процесс считается "zombie". Замечание: если вы вызовите ps, дочерний процесс получает 'Z' в своем статусе, показывающее такое состояние. Несмотря на то, что zombie не работает, он все еще занимает запись в таблице процессов (он не потребляет другие ресурсы, однако некоторые утилиты могут показывать фиктивные цифры, например использование CPU; это связано с тем, что некоторые части записи таблицы процессов накладываются с учетной информацией для экономии места).

Это нехорошо, так как в таблице процессов фиксированное количество записей, и есть возможность их исчерпания в системе. Даже если система на завершает работу, существует ограничение на количество процессов, которое пользователь может запустить, что обычно меньше предела системы. Это одна из причин, почему, кстати, вы должны всегда проверять, успешно ли было выполнение fork()!

Если родитель завершается без вызова wait(), то дочерний процесс "усыновляется" параметром init, который обеспечивает работу, необходимую для очистки после дочернего процесса (эта специальная системная программа с идентификатором ID 1 - на самом деле это первая программа, которая запускается после загрузки системы).

1.6.2 Как предотвратить возникновение зомби?
--------------------------------------------

Нужно гарантировать, чтобы ваш родительский процесс вызвал wait() (или waitpid(), wait3(), и т. п.) для каждого дочернего процесса, который завершается; или, в некоторых системах, вы можете информировать систему, что не заинтересованы в информации статуса выхода дочерних процессов.

Другой метод - вызывать fork() дважды, и немедленный выход из дочернего процесса. Это приводит к "сиротству" процесса-внука, так что процесс init отвечает за его очистку. Код для этого см. в функции fork2(), в примерах кода [4].

Чтобы игнорировать состояния выхода дочернего процесса вам нужно сделать следующую проверку (см. manpages вашей системы, чтобы проверить, что это работает):

      struct sigaction sa;
      sa.sa_handler = SIG_IGN;
   #ifdef SA_NOCLDWAIT
      sa.sa_flags = SA_NOCLDWAIT;
   #else
      sa.sa_flags = 0;
   #endif
      sigemptyset(&sa.sa_mask);
      sigaction(SIGCHLD, &sa, NULL);

Если эта проверка успешна, то предотвращается работа функций wait(); если любая из них вызвана, то она будет ждать пока ВСЕ дочерние процессы не завершатся, после чего вернет ошибку со статусом errno == ECHILD.

Другие техники заключаются в перехвате сигнала SIGCHLD, и сделать вызов обработчика сигнала waitpid() или wait3(). См. примеры [4] для полного кода программы.

1.7 Как запустить мою программу в качестве сервиса/демона (daemon)?
===================================================================

Процессом "демона" (daemon) в терминологии Unix-систем называют фоновый процесс, который не принадлежит сессии терминала (в терминологии Windows-систем это называется сервис). Многие системные сервисы выполняются демонами - службы сетей, печать, журналирование, запуск заданий crontab и т. д.

Простой вызов программы в фоновом режиме на самом деле не подходит для всех долго работающих (постоянно работающих) программ; неправильным будет отключать процесс от сессии терминала, который его запустил. Также обычный способ запуска демонов - просто выдать команду вручную или из rc-скрипта; ожидается, что демон сам переведет себя в фоновое выполнение.

Вот шаги, чтобы стать демоном:

  1. Выполнить fork(), чтобы родительский процесс мог выйти, это вернет управление в командную строку или шелл, откуда была запущена ваша программа. Этот шаг нужен, чтобы новый процесс не стал лидером группы процессов. Следующий шаг setsid() потерпит неудачу, если вы являетесь лидером группы процессов.

  2. Выполните setsid(), чтобы стать группой процессов (process group) и лидером группы сеансов (session group). Поскольку управляющий терминал связан с сессией,  и эта новая сессия пока не приобрела управляющий терминал, у нашего процесса теперь нет управляющего терминала, что хорошая вещь для демонов.

  3. Еще раз fork(), так что родитель (лидер session group) может сделать выход. Это означает что мы, как non-session group leader, не сможем никогда заново получить управляющий терминал.

  4. Выполните chdir("/") чтобы гарантировать, что наш процесс не удерживает в использовании какую-либо директорию. Неспособность сделать это может привести к тому, что администратор не сможет отмонтировать файловую систему, потому что это был наш текущий каталог. Замечание: эквивалентно, мы могли бы перейти на любой каталог, содержащий файлы, важные для работы демона.

  5. Вызовите umask(0), так мы получим полное управление над разрешением всего, что мы записываем. Мы не знаем, какой umask мы можем унаследовать. Замечание: этот шаг не обязателен.

  6. Вызовите close() fds 0, 1 и 2 (дескрипторы файлов). Это освободит стандартные потоки in, out и error, которые мы унаследовали из нашего родительского процесса. Мы не можем знать, куда эти fds могли быть перенаправлены. Обратите внимание, что многие демоны используют sysconf(), чтобы определить лимит _SC_OPEN_MAX. Значение _SC_OPEN_MAX покажет вам максимальное количество открытых файлов/процессов. Затем в цикле демон может закрыть все возможные дескрипторы файлов. Вы должны решить, нужно это делать или нет. Если вы считаете, что могут быть открыты дескрипторы файлов, вы должны закрыть их, поскольку существует ограничение на количество конкурентных дескрипторов файлов.

  7. Создайте новые открытые дескрипторы для stdin, stdout и stderr. Даже если вы не планируете их использовать, все равно их открытие это хорошая идея. Точная их обработка - вопрос вкуса; если у вас есть файл лога, например, то вы можете захотеть открыть его как stdout или stderr, и открыть /dev/null как stdin. Альтернативно вы можете открыть /dev/console как stderr и/или stdout, и /dev/null как stdin, или любую другую комбинацию, которая имеет смысл для вашего конкретного демона.

Почти ничего из этого не требуется (или является желательным), если ваш демон запускается через inetd. В этом случае stdin, stdout и stderr все настроены для того, чтобы ссылались на сетевое соединение, и манипуляция вызовами fork() и session не должны выполняться (чтобы не запутывать inetd). Остаются полезными только шаги chdir() и umask().

1.8 Как получить информацию о процессе в системе, как это делает утилита ps?
============================================================================

На самом деле вы не хотите это делать.

Самый портируемый способ, безусловно, это сделать popen(pscmd, "r") и после этого пропарсить вывод (pscmd на системах SysV должна выглядеть наподобие "ps -ef"; на системах BSD есть множество вариантов отображения, выберите какой-нибудь один).

В коде примеров [4] есть две полные версии этих методов. Один пример для SunOS 4, который требует разрешений для запуска, и использует подпрограммы kvm_* для чтения информации из структур данных ядра. Другой пример для систем SVR4 (включая SunOS 5), который использует файловую систему /proc.

Проще всего это работает на системах, где есть /proc стиля SVR4.2; просто прочитайте структуру psinfo_t из файла /proc/PID/psinfo для каждого интересующего PID. Однако этот метод, хотя возможно самый чистый, пожалуй меньше всего поддерживается (на /proc версии FreeBSD вы читаете не очень хорошо документированную печатаемую строку из /proc/PID/status; на Linux есть что-то подобное).

1.9 Как по идентификатору pid определить работающую программу?
==============================================================

Используйте kill() со значением 0 для номера сигнала.

Могут быть 4 возможных варианта результата этого вызова:

   * kill() возвратил 0.
        - это подразумевает, что существует процесс с указанным PID, и система позволит вам посылать ему сигналы. Это зависит от системы, может ли процесс быть зомби.

   * kill() возвратил -1, errno == ESRCH.
        - либо нет процесса с указанным PID, либо настройка безопасности приводит к тому, что система запрещает определить его существование (на некоторых системах процесс может быть зомби).

   * kill() возвратил -1, errno == EPERM.
        - система не позволяет вам прибить указанный процесс. Это означает, что либо процесс существует (и снова, он может быть зомби), или существуют драконовские настройки безопасности (т. е. вашему процессу не разрешено посылать сигналы кому попало, "anybody").

   * kill() возвратил -1, с каким-то другим значением errno.
        - у вас неприятности!

Наиболее используемой техникой является предположение, что успех или неудача с EPERM подразумевает, что процесс существует, а любая другая ошибка подразумевает, что его нет.

Существует альтернатива, если вы пишете специально для системы (или всех таких систем), которая предоставляет файловую систему /proc: может работать проверка существования /proc/PID.

1.10 Что означает возвращаемое значение system/pclose/waitpid?
==============================================================

Возвращаемое значение system(), pclose(), или waitpid() по-видимому, не является выходным значением моего процесса... или выходное значение сдвигается влево на 8 бит... в чем дело?

Страничка man права, и вы тоже! Если вы прочитаете документацию для waitpid(), то обнаружите, что значение возврата закодировано. Значение, возвращаемое процессом, обычно находится в верхних 16 битах, и остальная часть используется для других целей. Однако вы не можете полагаться на это, если хотите обеспечить портируемость кода, поэтому рекомендуется использовать предоставленные макросы. Обычно они задокументированы в wait() или wstat.

Макросы, определенные для этой цели (находятся в < sys/wait.h>), здесь stat это значение, возвращаемое из waitpid():

WIFEXITED(stat)
     Не равно 0, если дочерний процесс нормально завершился.

WEXITSTATUS(stat)
     Код завершения, возвращенный дочерним процессом.

WIFSIGNALED(stat)
     Не ноль, если дочерний процесс был прерван по сигналу.

WTERMSIG(stat)
     Номер сигнала, который завершил дочерний процесс.

WIFSTOPPED(stat)
     Не ноль, если дочерний процесс остановлен.

WSTOPSIG(stat)
     Номер сигнала, который остановил дочерний процесс.

WIFCONTINUED(stat)
     Не ноль, если был статус для продолжающегося дочернего процесса.

WCOREDUMP(stat)
     Если WIFSIGNALED(stat) не ноль, то если это не равно 0, то процесс оставил после себя дамп ядра.

1.11 Как определить используемое процессом количество памяти?
=============================================================

См. getrusage(), если эта функция доступна.

1.12 Почему процессы никогда не уменьшаются в размере?
======================================================

Когда вы освобождаете ранее выделенную из кучи память вызовом free(), на почти всех системах не уменьшается количество памяти, которое использует ваша программа. Освобожденная через free() память все еще остается частью адресного пространства процесса, и будет использоваться для удовлетворения будущим запросам malloc().

Если вы реально хотите вернуть память системе, то рассмотрите использование mmap() для выделения приватных анонимных отображений памяти. Когда отображение такой памяти отменяется (memory unmap), память реально возвращается обратно системе. Определенные реализации malloc() (например в библиотеке GNU C) автоматически используют mmap(), когда это доступно, для выполнения больших выделений памяти; затем эти блоки возвращаются системе после вызова free().

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

1.13 Как поменять имя моей программы (которое отображается утилитой ps)?
========================================================================

На системах BSD программа ps реально просматривает адресное пространство запущенного процесса, чтобы найти текущее argv[], и показывает его. Это позволяет программе поменять её "имя" путем простой модификации argv[].

На системах SysV имя команды и обычно первые 80 байт параметров сохраняются в u-area процесса, и это не может быть напрямую изменено. Для изменения может быть системный вызов (маловероятно), но в противном случае единственным способом будет выполнить exec(), или сделать запись в память ядра (это опасно, и возможно только под управлением root).

Некоторые системы (в частности Solaris) могут иметь две отдельные версии ps, одна в /usr/bin/ps, у которой поведение по типу SysV, и другая в /usr/ucb/ps, у которой поведение как в BSD. На этих системах, если вы поменяете argv[], то BSD-версия ps покажет это изменение, а SysV-версия не покажет.

Проверьте, есть ли на вашей системе функция setproctitle().

1.14 Как найти исполняемый файл процесса?
=========================================

Это хороший кандидат на список вопросов, часто остающихся без ответа (FUQ, Frequently Unanswered Questions), потому что сам факт постановки такого вопроса означает, что дизайн программы хромает :-).

Вы можете сделать "лучшее предположение", просмотрев значение argv[0]. Если в нем содержится  '/', то это вероятно абсолютный или относительный (по отношению к текущему рабочему каталогу) путь до исполняемого файла программы. Если же нет, то вы можете имитировать поиск в переменной окружения PATH шелла, чтобы найти вашу программу. Однако успех не гарантируется, поскольку можно вызывать программы с произвольными значениями argv[0], и в любом случае исполняемый файл может быть переименован или удален с момента своего запуска.

Если все, что вам нужно, это вывести подходящее имя запуска с сообщениями об ошибке, то самый лучший способ это сделать main() сохранение значения argv[0] в глобальной переменной, чтобы потом его использовать во всей программе. Хотя и тут нет полной гарантии, что сохраненное значение argv[0] окажется значимым, это лучший вариант, доступный в большинстве случаев.

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

Менее распространенная, но более легитимная причина задавать такой вопрос - позволить программе запускать саму себя, т. е. выполнять exec(); этот метод используется (например, некоторыми версиями sendmail) для полной переинициализации процесса (т. е. если демон получает сигнал SIGHUP).

1.14.1 Куда поместить конфигурационную информацию программы (конфигурационные файлы)?
-------------------------------------------------------------------------------------

Корректная директория для этого обычно зависит от стиля вашей Unix-системы; это могут быть пути /var/opt/PACKAGE, /usr/local/lib, /usr/local/etc, или любые другие возможные каталоги хранения конфигураций. Пользовательские конфигурационные файлы обычно "скрыты" через имена с точкой, и находятся в корне домашней директории $HOME (т. е. к примеру $HOME/.exrc).

С точки зрения пакета, который ожидается использовать в широком диапазоне систем, обычно подразумевается, что расположение любых файлов конфигурации будет скомпилировано по умолчанию, возможно с использованием опции --prefix скрипта configure (это делают скрипты Autoconf). Вы можете захотеть поменять это во время работы программы (runtime) через переменную окружения. Замечание: если вы не используете скрипт configure, то поместите значение по умолчанию в Makefile как опцию -D компиляции, или поместите в заголовочный файл config.h, или сделайте что-то подобное.

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

1.15 Почему мой процесс не получает SIGHUP, когда его родитель умирает?
=======================================================================

Потому что этого не должно быть в принципе.

По соглашению сигнал SIGHUP означает "линия терминала разорвана". Этот сигнал не имеет ничего общего с родительскими процессами, и обычно генерируется драйвером tty (и доставляется в foreground-группу процессов).

Однако, как часть системы управления сессией, есть ровно 2 случая, когда SIGHUP посылается в случае смерти процесса:

   * Когда умерший процесс является лидером сессии, которая подключена к устройству терминала, SIGHUP посылается всем процессам в foreground-группе процессов этого устройства терминала.
   * Когда смерть процесса приводит к тому, что группа процессов становится потерянной, и один или несколько процессов в потерянной группе ОСТАНАВЛИВАЕТСЯ, тогда SIGHUP и SIGCONT отправляются всем членам потерянной группы. Замечание: потерянная группа процессов - это группа процессов, в которой ни один процесс в группе не имеет родителя, который является частью одного сеанса, но не одной и той же группы процессов.

1.16 Как прибить всех потомков процесса?
========================================

Не существует полного общего подхода для такого действия. Хотя вы можете определить взаимосвязи между процессами парсингом вывода ps, это ненадежно в том смысле, что ps предоставляет только снимок системы.

Однако, если вы запускаете дочерний процесс, который в свою очередь порождает собственные дочерние субпроцессы, и вы хотите прибить все это за один раз, то решение состоит в том, чтобы поместить суброцесс в новую группу процессов, и убить эту группу при необходимости.

Для создания группы процессов предпочтительно использовать функцию setpgid(). Используйте её вместо setpgrp(), потому что последняя отличается между системами (на некоторых системах "setpgrp();" это эквивалент "setpgid(0,0);", на других же setpgrp() и setpgid() идентичны).

См. код Job Control в примерах [4].

Помещение подпроцесса в собственную группу процессов имеет ряд последствий. В частности, если новая группа процессов явно не вынесена на передний план (foreground), она будет рассматриваться как фоновое задание со следующими последствиями:

   * Группа будет остановлена с сигналом SIGTTIN, если сделана попытка чтения из терминала.
   * Если tostop установлено в режимах терминала, то группа будет остановлена с сигналом SIGTTOU при попытке записи в терминал (попытка изменить режим терминала также приведет к этому, независимо от текущей установки tostop).
   * Субпроцесс не получит сигнал клавиатуры из терминала (т. е. сигнал SIGINT или SIGQUIT).

Во многих приложениях ввод и вывод все равно будет перенаправлен, так что наиболее значимым будет отсутствие сигналов от клавиатуры. Родительское приложение должно организовать перехват как минимум сигналов SIGINT и SIGQUIT (и также желательно SIGTERM), и очистку любых фоновых заданий по мере необходимости.

[Словарик]

AIX аббревиатура от Advanced Interactive eXecutive) - разновидность операционной системы UNIX производства компании IBM (из Википедии).

BTW By The Way, устойчивое выражение, которое переводится как "кстати", "кстати говоря", "между прочим".

FD File Descriptor, идентификатор открытого файла.

FIFO First Input First Output, "первый зашел, первый вышел" - принцип организации буфера ввода или вывода для данных.

IPC Inter-Process Communication, взаимодействие между процессами.

IRIX  операционная система, используемая на рабочих станциях и серверах фирмы Silicon Graphics (SGI) архитектуры MIPS (из Википедии).

NFS Network File System, сетевая файловая система.

pty программный псевдо-терминал, т. е. PseudoTeletYpe (PTY), который позволяет эмулировать TTY.

RTFM Read The Fucking Manual, прочтите же наконец документацию.

SVR4 то же самое, что SysV версии 4.

SysV UNIX System V, одна из коммерческих версий операционной системы Unix.

tty TTY это аббревиатура от TeleTYpe или TeleTYpewriter. По сути TTY это устройства, которые позволяют локально или дистанционно вводить и выводить символы.

UCB или AGL UCB, Automotive Grade Linux Unified Code Base: дистрибутив Linux, созданный с нуля благодаря совместным усилиям автопроизводителей и поставщиков по предоставлению современного информационно-развлекательного оборудования.

UUCP аббревиатура Unix-to-Unix CoPy - команда копирования файлов между двумя компьютерами под управлением операционной системы UNIX, использующая одноимённый протокол. Позже появились реализации этого протокола под другие операционные системы, в том числе DOS, Windows, OS/2.

[Ссылки]

1. Unix Programming FAQ (v1.37) site:opennet.ru.
2. Альтернатива функции Sleep для реализации задержки в миллисекундах.
3. Linux: задержки в программе на языке C.
4. FAQ программирования Linux: примеры.
5Опции GCC для поддержки отладки.

 

Добавить комментарий


Защитный код
Обновить

Top of Page