Главная

Популярная публикация

Научная публикация

Случайная публикация

Обратная связь

ТОР 5 статей:

Методические подходы к анализу финансового состояния предприятия

Проблема периодизации русской литературы ХХ века. Краткая характеристика второй половины ХХ века

Ценовые и неценовые факторы

Характеристика шлифовальных кругов и ее маркировка

Служебные части речи. Предлог. Союз. Частицы

КАТЕГОРИИ:






Пример Создания Процесса




Вот пример программы, показывающий, как Вы могли бы написать функцию, подобную встроенной системе. Она выполняет аргумент command, используя " sh -c command ".

#include <stddef.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define SHELL "/bin/sh" int my_system (const char *command) { int status; pid_t pid; pid = fork (); if (pid == 0) { execl (SHELL, SHELL, "-c", command, NULL); _exit (EXIT_FAILURE); } else if (pid < 0) status = -1; else if (waitpid (pid, &status, 0)!= pid) status = -1; return status; }

Имеется две вещей, на которые Вы должны обратить внимание в этом примере.

Не забудьте, что первый аргумент argv, представляет имя выполняемой программы. Именно поэтому, в обращении к execl, SHELL обеспечена один раз, чтобы назвать выполняемую программу, и второй раз, чтобы обеспечить значение для argv [0].

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

Вызовите _exit, чтобы выполнить это. Причина для использования _exit вместо exit состоит в том, чтобы избежать flush полностью буферизированных потоков типа stdout. Буфера этих потоков возможно содержат данные, которые были скопированы из родительского процесса функцией fork, эти данные будут выводиться в конечном счете родительским процессом. Вызов exit в дочернем вывел бы данные дважды. См. Раздел 22.3.5 [Внутренняя организация Окончания].

 

Демоны

Статья из серии " Программирование для Linux ", журнал Linux Format

Андрей Боровский, symmetrica.net

— Я и есть демон! Слушай, малыш, в моем мире демоном был бы ты, но в текущий момент я в твоем мире, поэтому демон я.
Роберт Асприн, Другой отличный миф

Демонами в мире Unix традиционно называются процессы, которые не взаимодействуют с пользователем напрямую. У процесса-демона нет управляющего терминала и нет, соответственно, пользовательского интерфейса. Для управления демонами приходится использовать другие программы. Само название «демоны» возникло благодаря тому, что многие процессы этого типа большую часть времени проводят в ожидании какого-то события. Когда это событие наступает, демон активизируется (выпрыгивает, как чертик из табакерки), выполняет свою работу и снова засыпает в ожидании события. Следует отметить, что многие демоны, такие как, например, Web-сервер или сервер баз данных, могут отбирать на себя практически все процессорное время и другие ресурсы системы. Такие демоны гораздо больше работают, чем спят.

Вам, уважаемый читатель, вряд ли придется заниматься созданием собственного демона, поскольку круг задач, для которых может понадобиться демон, не так уж и велик. Область применения демонов – создание таких приложений, которые могут, и должны, выполняться без участия пользователя. Обычно это разного рода серверы. Тем не менее, демоны задействуют многие важные элементы системы и понимание принципов работы демонов способствует пониманию принципов работы Unix/Linux в целом (помимо прочего, добрый демон поможет нам изучить некоторые особенности применения сигналов, с которыми мы ранее не сталкивались). В качестве примера демона мы рассмотрим простой (очень простой) сетевой сервер aahzd (надеюсь, что поклонники творчества Асприна меня поймут и простят), способный принимать запросы клиентов и возвращать ответы. Исходный код нашего сервера (вы найдете его на диске в файле aahzd.c) представляет собой доработанный исходный код открытого демонстрационного (простите за невольный каламбур) демона, написанного Давидом Жилье (David Gillies). Те места исходного текста, в которые я внес изменения, помечены в файле aahzd.c комментарием “Note by A.B....” Поскольку разговор о демонах вызывает у меня трепет, я не буду долго теоретизировать и сразу перейду к исходному коду (функции main() нашего демона)

volatile sig_atomic_t gGracefulShutdown=0;volatile sig_atomic_t gCaughtHupSignal=0;int gLockFileDesc=-1;int gMasterSocket=-1;const int gaahzdPort=30333;const char *const gLockFilePath = "/var/run/aahzd.pid";int main(int argc,char *argv[]){ int result; pid_t daemonPID; if (argc > 1) { int fd, len; pid_t pid; char pid_buf[16]; if ((fd = open(gLockFilePath, O_RDONLY)) < 0) { perror("Lock file not found. May be the server is not running?"); exit(fd); } len = read(fd, pid_buf, 16); pid_buf[len] = 0; pid = atoi(pid_buf); if(!strcmp(argv[1], "stop")) { kill(pid, SIGUSR1); exit(EXIT_SUCCESS); } if(!strcmp(argv[1], "restart")) { kill(pid, SIGHUP); exit(EXIT_SUCCESS); } printf ("usage %s [stop|restart]\n", argv[0]); exit (EXIT_FAILURE); } if((result = BecomeDaemonProcess(gLockFilePath, "aahzd", LOG_DEBUG, &gLockFileDesc, &daemonPID))<0) { perror("Failed to become daemon process"); exit(result); } if((result = ConfigureSignalHandlers())<0) { syslog(LOG_LOCAL0|LOG_INFO, "ConfigureSignalHandlers failed, errno=%d", errno); unlink(gLockFilePath); exit(result); } if((result = BindPassiveSocket(INADDR_ANY, gaahzdPort, &gMasterSocket))<0) { syslog(LOG_LOCAL0|LOG_INFO, "BindPassiveSocket failed, errno=%d", errno); unlink(gLockFilePath); exit(result); } do { if(AcceptConnections(gMasterSocket)<0) { syslog(LOG_LOCAL0|LOG_INFO,"AcceptConnections failed, errno=%d",errno); unlink(gLockFilePath); exit(result); } if((gGracefulShutdown==1)&&(gCaughtHupSignal==0)) break; gGracefulShutdown=gCaughtHupSignal=0; } while(1); TidyUp(); return 0;}

Сейчас мы пропустим блок операторов if (argc > 1) {...} (мы вернемся к нему позже) и рассмотрим основные этапы работы демона. Функция BecomeDaemonProcess() превращает обычный консольный процесс Linux в процесс-демон. Функция ConfigureSignalHandlers() настраивает обработчики сигналов процесса-демона, а функция BindPassiveSocket() открывает определеный порт TCP/IP для прослушивания входящих запросов. Далее следует цикл, в котором сервер обрабатывает запросы. Многие сетевые серверы, получив запрос, создают дочерний процесс для его обработки. Таким образом достигается возможность параллельной обработки запросов. Некоторые серверы используют для параллельной обработки запросов потоки. Что касается нашего сервера, то из соображений простоты он обрабатывает запросы в последовательном (блокирующем) режиме. Мы ведь не ожидаем, что наш демонстрационный сервер будет получать много запросов, не так ли?

Нормальный выход из цикла обработки запросов происходит при получении процессом сигнала SIGUSER1. После выхода из цикла процесс вызывает функцию TidyUp() и завершает работу. Мы, безусловно, можем завершить процесс-демон, послав ему сигнал SIGKILL (SIGTERM и некоторые другие), но пользовательский сигнал SIGUSER1 гарантирует вежливое завершение нашего демога. «Вежливое завершение» означает, что сервер ответит на текущий запрос перед тем, как завершиться и удалит свой pid- файл.

Рассмотрим теперь подробнее функцию BecomeDaemonProcess(), благодаря которой обычный процесс Linux становится демоном. Мы не будем перепечатывать тексты функций целиком, так как иначе статья будет состоять исключительно из листингов (на самом деле, длина листингов даже превышает пространство, выделенное под статью). По этой причине во время чтения статьи рекомендуется постоянно иметь под рукой прилагаемый исходный текст.

В строке

chdir("/");

Мы делаем корневую директорию текущей директорией процесса-демона. Будучи запущен, наш демон может работать вплоть до перезагрузки системы, поэтому его текущая диретокрия должна принадлежать файловой системе, которая не может быть размонтирована. Далее следует вызов lockFD = open(lockFileName, O_RDWR|O_CREAT|O_EXCL, 0644);

Каждый процесс-демон создает так называемый pid-файл (или файл блокировки). Этот файл обычно содержится в директории /var/run и имеет имя daemon.pid, где “daemon” соответствует имени демона. Файл блокировки содержит значение PID процесса демона. Этот файл важен по двум причинам. Во-первых, его наличие позволяет установить, что в системе уже запущен один экземпляр демона. Большинство демонов, включая наш, должны выполняяться не более чем в одном экземпляре (это логично, если учесть, что демоны часто обращаются к неразделяемым ресурсам, таким, как сетевые порты). Завершаясь, процесс-демон удаляет pid-файл, указывая тем самым, что можно запустить другой экземпляр процесса. Однако, работа демона не всегда завершается нормально, и тогда на диске остается pid-файл несуществующего процесса. Это, казалось бы, может стать непреодолимым препятствием для повторного запуска демона, но на самом деле, демоны успешно справляются с такими ситуациями. В процессе запуска демон проверяет наличие на диске pid-файла с соответствующим именем. Если такой файл существует, демон считывает из него значение PID и с помощью функции kill(2) проверяет, существует ли в системе процесс с указанным PID. Если процесс существует, значит, пользователь пытается запустить демон повторно. В этом случае программа выводит соответствующее сообщение и завершается. Если процесса с указанным PID в системе нет, значит pid-файл принадлежал аварийного завершенному демону. В этой ситуации программа обычно советует пользователю удалить pid-файл (ответственность в таких делах всегда лучше переложить на пользователя) и попытаться запустить ее еще раз. Может, конечно, случиться и так, что после аварийного завершения демона на диске останется его pid-файл, а затем какой-то другой процесс получит тот же самый PID, что был у демона. В этой ситуации для вновь запускаемого демона все будет выглядеть так, как будто его копия уже работает в системе, и запустить демон повторно вы не сможете. К счастью, описанная ситуация крайне маловероятна.

Вторая причина, по которой файл блокировки считается полезным, заключается в том, что с помощью этого файла мы можем быстро выяснить PID демона, не прибегая к команде ps.

Далее наш демон вызывает функцию fork(3), которая создает копию его процесса. Родительский процесс при этом завершается:

curPID=fork();switch(curPID) { case 0: /* мы в дочернем процессе */ break; case -1: /* ошибка fork() - случилось что-то страшное */ fprintf(stderr,"Error: initial fork failed: %s\n", strerror(errno)); return -1; break; default: /* мы в родительском процессе, завершаем его */ exit(0); break;}

Делается это для того чтобы процесс-демон отключился от управляющего терминала. С каждым терминалом Unix связан набор групп процессов, именуемый сессией. В каждый момент времени только одна из групп процессов, входящих в сессию, имеет доступ к терминалу (то есть, может выполнять ввод/вывод с помощью терминала). Эта группа именуется foreground (приоритетной). В каждой сессии есть процесс-родоначальник, который называется лидером сессии. Если процесс-демон запущен с консоли, он, естественно, становится частью приоритетной группы процессов, входящих в сессию соответствующего терминала.

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

if(setsid()<0) return -1;

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

Стоит отметить, что теперь наш демон получил новый PID, который мы снова должны записать в pid-файл демона. Мы записываем значение PID в файл в строковом виде (а не как переменную типа pid_t). Делается это для удобства пользователя, чтобы значение PID из pid-файла можно было прочитать с помощью cat. Например:

kill `cat /var/run/aahzd.pid `

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

numFiles = sysconf(_SC_OPEN_MAX);for(i = numFiles-1; i >= 0; --i) { if(i!= lockFD) close(i);}

Функция sysconf() с параметром _SC_OPEN_MAX возвращает максимально возможное количество дескрипторов, которые может открыть наша программа. Мы вызываем функцию close() для каждого дескриптора (независимо от того, открыт он или нет), за исключением дескриптора pid- файла, который должен оставаться открытым.

Во время работы демона дескрипторы стандартных потоков ввода, вывода и ошибок также должны быть открыты, поскольку они необходимы многим функциям стандартной библиотеки. В то же время, эти дескприторы не должны указывать на какие-либо реальные потоки ввода/вывода. Для того, чтобы решить эту задачу, мы закрываем первые три дескриптора, а затем снова открываем их, указывая в качестве имени файла /dev/null:

stdioFD = open("/dev/null", O_RDWR);dup(stdioFD);dup(stdioFD);

Теперь мы можем быть уверены, что демон не получит доступа к какому- либо терминалу. Тем не менее, у демона должна быть возможность выводить куда-то сообщения о своей работе. Традиционно для этого используются файлы журналов (log-файлы). Файлы журналов для демона подобны черным ящикам самолетов. Если в работе демона произошел какой-то сбой, пользователь может проанализировать файл журнала и (при определенном везении) установить причину сбоя. Ничто не мешает нашему демону открыть свой собственный файл журнала, но это не очень удобно. Большинство демонов пользуются услугами утилиты syslog, ведущей журналы множества системных событий. Мы открываем доступ к журналу syslog с помощью функции openlog(3):

openlog(logPrefix, LOG_PID|LOG_CONS|LOG_NDELAY|LOG_NOWAIT, LOG_LOCAL0);(void) setlogmask(LOG_UPTO(logLevel));

Первый параметр функции openlog() – префикс, который будет добавляться к каждой записи в системном журнале. Далее следуют различные опции syslog. Функция setlogmask(3) позволяет установить уровень приоритета сообщений, которые записываются в журнал событий. При вызове функции BecomeDaemonProcess() мы передаем в параметре logLevel значение LOG_DEBUG. В сочетании с макросом LOG_UPTO это означает, что в журнал будут записываться все сообщения с приоритетом, начиная с наивысшего и заканчивая LOG_DEBUG.

Последнее, что нам нужно сделать для «демонизации» процесса – вызывать функцию setpgrp();

Этот вызов создает новую группу процессов, идентификатором которой является идентификатор текущего процесса. На этом работа функции BecomeDaemonProcess() завершается, так как теперь наш процесс стал настоящим демоном.

Функция ConfigureSignalHandlers() настраивает обработчики сигналов. Сигналы, которые получит наш демон, можно разделить на три группы: игнорируемые, «фатальные» и обрабатываемые. Вызывая функцию signal(SIGUSR2, SIG_IGN);

мы указываем, что наш демон должен игнорировать сигнал SIGUSR2. Аналогично мы поступаем с сигналами SIGPIPE, SIGALRM, SIGTSTP SIGPROF, SIGCHLD. Сигналы SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGIOT, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT, SIGCONT, SIGPWR и SIGSYS относятся к категории «фатальных». Мы не можем их игнорировать, но и продолжать выполнение процесса-демона после получения одного из этих сигналов нежелательно. Мы назначаем всем этим сигналам обработчик FatalSigHandler, например:

signal(SIGQUIT, FatalSigHandler);

Функция-обработчик FatalSigHandler() записывает в журнал событий информацию о полученном сигнале, и затем завершает процесс, вызвав перед этим функции closelog() и TidyUp(), которые высвобождают все занятые процессом ресурсы:

void FatalSigHandler(int sig) {#ifdef _GNU_SOURCE syslog(LOG_LOCAL0|LOG_INFO,"caught signal: %s – exiting",strsignal(sig));#else syslog(LOG_LOCAL0|LOG_INFO,"caught signal: %d – exiting",sig);#endif closelog(); TidyUp(); _exit(0);}

На те три сигнала, которые относятся к категории обрабатываемых, – SIGTERM, SIGUSR1 и SIGHUP, демон реагирует по-разному:

sigtermSA.sa_handler = TermHandler;sigemptyset(&sigtermSA.sa_mask);sigtermSA.sa_flags = 0;sigaction(SIGTERM, &sigtermSA, NULL);sigusr1SA.sa_handler = Usr1Handler;sigemptyset(&sigusr1SA.sa_mask);sigusr1SA.sa_flags = 0;sigaction(SIGUSR1, &sigusr1SA, NULL);sighupSA.sa_handler = HupHandler;sigemptyset(&sighupSA.sa_mask);sighupSA.sa_flags = 0;sigaction(SIGHUP, &sighupSA, NULL);

Обработчик TermHandler() вызывает функцию TidyUp() и завершает процесс. Обработчик Usr1Handler() делает в системном журнале запись о вежливом завершении процесса и присваивает переменной gGracefulShutdown значение 1 (что, как вы помните, приводит к выходу из цикла обработки запросов, когда цикл будет готов к этому). Обработчик сигнала HupHandler() также делает запись в системном журнале, после чего присваивает значение 1 переменным gGracefulShutdown и gCaughtHupSignal. В реальной жизни получение сигнала SIGHUP приводит к перезапуску демона, который сопровождается повторным прочтением файла конфигурации (который обычно читается демоном именно во время запуска) и переустановкой значений записанных в нем параметров. Именно необходимость прочесть повторно файл конфигурации является наиболее частой причиной перезапуска демонов. У нашего демона файла конфигурации нет, так что в процессе перезапуска делать ему особенно нечего.

Обратите внимание на тип переменных gGracefulShutdown и gCaughtHupSignal. С типом sig_atomic_t мы раньше не встречались. Применение этого типа гарантирует, что чтение и запись данных в переменные gGracefulShutdown и gCaughtHupSignal будет выполняться атомарно, одной инструкцией процессора, которая не может быть прервана. Атомарность при работе с переменными gGracefulShutdown и gCaughtHupSignal важна потоуму, что к ним могут одновременно получить доступ и обработчики сигналов, и главная функция программы. По этой же причине мы помечаем указанные переменные ключевым словом volatile.

Функция BindPassiveSocket() открывает для прослушивания порт сервера (в нашем случае это порт 30333) на всех доступных сетевых интерфейсах и возвращает соответствующий сокет:

int BindPassiveSocket(const int portNum, int *const boundSocket){ struct sockaddr_in sin; int newsock, optval; size_t optlen; memset(&sin.sin_zero, 0, 8); sin.sin_port = htons(portNum); sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(INADDR_ANY); if((newsock= socket(PF_INET, SOCK_STREAM, 0))<0) return -1; optval = 1; optlen = sizeof(int); setsockopt(newsock, SOL_SOCKET, SO_REUSEADDR, &optval, optlen); if(bind(newsock, (struct sockaddr*) &sin, sizeof(struct sockaddr_in))<0) return -1; if(listen(newsock,SOMAXCONN)<0) return -1; *boundSocket = newsock; return 0;}

Тем, кто читал статью этой серии, посвященную сокетам, должно быть понятно, что здесь происходит. Отметим только одну интересную деталь. Если предыдущий, уже закрытый, сокет, связанный с данным портом, находится в состоянии TIME_WAIT, между закрытием старого и открытием нового сокета может произойти задержка, равная двум периодам жизни сегмента (задержка может составлять до двух минут). Для того, чтобы при повторном запуске демона нам не пришлось ждать, мы используем функцию setsockopt() с параметром SO_REUSEADDR.

Функция AcceptConnections() обрабатывает запросы последовательно, используя блокирующий вызов accept():

int AcceptConnections(const int master){ int proceed = 1, slave, retval = 0; struct sockaddr_in client; socklen_t clilen; while((proceed==1)&&(gGracefulShutdown==0)) { clilen = sizeof(client); slave = accept(master,(struct sockaddr *)&client,&clilen); if(slave<0) { /* ошибка accept() */ if(errno == EINTR) continue; syslog(LOG_LOCAL0|LOG_INFO,"accept() failed: %m\n"); proceed = 0; retval = -1; } else { retval = HandleConnection(slave); if(retval) proceed = 0; } close(slave); } return retval;}

Это не лучший образ поведения демона, но если мы начнем описывать параллельную обработку запросов, редакция не выдержит. Переменная proceed, совместно с переменной gGracefulShutdown, указывает, должна ли программа продолжать обрабатывать запросы. Если очередной вызов connect() или HandleConnection() вернул сообщение об ошибке, этой переменной присваивается 0 и обработка запросов прекращается. Новый сокет, полученный в результате вызовы accept(), передается функции HandleConnection().

int HandleConnection(const int slave){ char readbuf[1025]; size_t bytesRead; const size_t buflen=1024; int retval; retval = ReadLine(slave, readbuf, buflen, &bytesRead); if(retval==0) WriteToSocket(slave, readbuf, bytesRead); return retval;}

Функция HandleConnection() считывает переданную клиентом строку и тут же возвращает ее клиенту. Затем функция AcceptConnections() закрывает соединение, открытое в результате вызова accept(). Функции ReadLine() и WriteToSocket() тривиальны, и рассматривать их мы не будем. Если где-то в цепочке вызовов AcceptConnections(), HandleConnection(), ReadLine() и WriteToSocket() возникла ошибка, информация об ошибке будет передаваться вверх по цепочке до тех пор, пока не достигнет функции main(). В функции main() эта информация приведет к немедленному завершению работы демона с соответствующей записью в журнал системных сообщений.

Рассмотрим, наконец, функцию TidyUp(), к которой обращаются многие функции сервера перед тем, как завершить его работу.

void TidyUp(void){ if(gLockFileDesc!=-1) { close(gLockFileDesc); unlink(gLockFilePath); gLockFileDesc=-1; } if(gMasterSocket!=-1) { close(gMasterSocket); gMasterSocket=-1; }}

Задача функции TidyUp() – «прибрать мусор» за процессом-демоном. В принципе, без этой функции можно обойтись, так как после завершения процесса система сама закроет все его дескрипторы, но правила хорошего тона требуют явного высвобождения всех ресурсов, выделенных явным образом.

Если вы скомпилируете программу-демон с помощью команды

gcc aahzd.c -o aahzd

То сможете запустить демон командой

# aahzd

Поскольку демон нуждается в доступе к директории /var/run, запускать его нужно в режиме root. Сразу после запуска программы вы снова увидите приглашение командной строки, что для демонов совершенно нормально. Если бы сервер aahzd выполнял что-нибудь полезное, команду его запуска можно было бы прописать в одном из сценариев запуска системы в директории /etc/init.d, но мы этого делать не будем. После того как сервер запущен, вы можете дать команду

telnet 127.0.0.1 30333

В результате будет установлено соединение с сервером. Напечатайте какую-нибудь последовательность символов в консоли telnet и нажмите «Вввод». В качестве ответа сервер возвратит напечатанную строку и закроет соединение.

Вернемся теперь к начальным строкам функции main(). Хотя мы можем получить PID демона из его pid-файла и управлять демоном с помощью команды kill, такой вариант нельзя назвать очень удобным. Часто для управления демоном используется сам исполнимый файл демона, запускаемый со специальными аргументами командной строки. Наш демон понимает две команды: stop (завершение работы демона) и restart (перезапуск). Посмотрим, как поведет себя демон, запущенный с аргументами командной строки. В этом случае в начале программы демон пытается считать значение PID из своего pid-файла. Если открыть pid-файл не удается, значит, скорее всего, демон не запущен, и управляющему режиму просто нечего делать. Если значение PID получено, процесс, управляющий демоном, посылает демону соответствующий сигнал с помощью функции kill().

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

 

If you're curious and want to see how a zombie looks like you can create a zombie process with this c program:

 

Code:

#include <stdlib.h>#include <sys/types.h>#include <unistd.h> int main (){ pid_t child_pid; child_pid = fork (); if (child_pid > 0) { sleep (60); } else { exit (0); } return 0;}


Save it as zombie.c, compile it with:


Code:

cc zombie.c -o zombie


and run it with:


Code:

./zombie


Now check the output of this command (within 1 minute) in another window:


Code:

ps -e -o pid,ppid,stat,cmd


The child process is marked as <defunct>, and its status code is Z, for zombie.

When the program exits, the child process is inherited by init. This process should cleans up the zombie proces automatically.

 

Процессы и потоки

Статья из серии " Программирование для Linux ", журнал Linux Format

Андрей Боровский, symmetrica.net

 

  Все процессы должны быть у нас под контролем
  М.С. Горбачев

Понятие процесса играет ключевую роль в современных ОС. Существует много определений процессов, я воспользуюсь простым определением, данным в [1] – процесс, это выполняющийся экземпляр программы. Хотя это определение, в общем, «работает», оно не совсем подходит к многопоточным программам Linux, о которых мы поговорим в следующей статье. Важный аспект процессов с точки зрения ОС – неделимость процесса относительно выделения ресурсов. Система выделяет ресурсы процессу в целом, а не его частям. Внутри процесса задача управления ресурсами ложится на программиста. Для каждого процесса ядро системы поддерживает специальную структуру данных task_struct, в которой хранятся важнейшие параметры процесса (процессы уровня пользователя не имеют доступа к этой структуре). В структуре task_struct есть специальное поле, в котором хранится численный идентификатор процесса (Process Identifier, PID). Именно этот идентификатор используется для обозначения процессов на уровне прикладного API Linux.

Под управлением процессами мы будем понимать, прежде всего, создание новых процессов и контроль их выполнения. Обычно новые экземпляры программы создаются с помощью вызова функции fork(2), с которой мы уже многократно встречались в статьях этой серии. Помимо функции fork() Linux предоставляет нам еще один, весьма тонкий инструмент - функцию clone(2). Эта функция позволяет настраивать разделение ресурсов между процессами, но не умеет раздваивать процесс в точке вызова, как это делает fork(). Специально для того, чтобы вас запутать, существует еще и системный вызов sys_clone(), который умеет раздваивать процессы в точке вызова. Вы, конечно, знаете, что, будучи вызвана один раз, функция fork() возвращает управления дважды, двум копиям исходного процесса (родительской и дочерней). Поскольку вы знаете так же, что эти две копии процесса похожи как две капли воды, будет полезнее перечислить ниже не сходства этих процессов, а их различия.

  • Родительскому процессу функция fork() возвращает PID дочернего процесса, а дочернему процессу – значение 0. Именно по возвращенному fork() значению процесс может узнать, дочерний он или родительский. В иерархии процессов родительский процесс оказывается родителем (странно, не правда ли) дочернего процесса. Дочерний процесс может получить PID родительского процесса с помощью вызова getppid(2), в то время как родительский процесс может узнать PID своего дочернего процесса только из результата fork(). Именно поэтому fork() может позволить себе не возвращать значение PID дочернему процессу, но обязана возвращать значение PID дочернего процесса родительскому.
  • Значения tms_utime, tms_stime, tms_cutime, и tms_cstime в дочернем процессе обнуляются.
  • Блокировки на уровне файлов (file locks), установленные в родительском процессе, в дочернем процессе снимаются (иначе сразу два процесса оказались бы владельцами блокировок).
  • Сигналы, обработка которых была отложена в родительском процессе в момент вызова fork(), в дочернем процессе сбрасываются.

Если вы хотите запустить из вашей программы экземпляр другой программы, вы должны пожертвовать для этого уже существующим процессом. Если вы хотите, чтобы новая программа работала одновременно со старой, нужно сначала раздвоить процесс с помощью fork(), а затем заместить образ программы в одной из копий образом новой программы. Для замены образа одной программы образом другой применяются функции семейства exec*. Функций этого семейства всего шесть: execl(), execlp(), execle(), execv(), execvp() и execv(). Если вы присмотритесь к названиям функций, то увидите, что первые три имеют имена вида execl*, а вторые три – имена вида execv*. Эти функции отличаются списком параметров, а также тем, как обрабатывается имя файла.

Рассмотрим, например, функции execve(2) и execvp(2). Заголовок функции execve(2) имеет вид:

int execve(const char *pathname, char *const argv[], char *const envp []);

Первый параметр функции execve(), - это имя исполнимого файла запускаемой программы (исполнимым файлом может быть двоичный файл или файл сценария оболочки, в котором в первой строке указан интерпретатор). Имя исполнимого файла для этой функции должно включать полный путь к файлу, (путь, начиная с корневого слеша, точки или тильды). Второй параметр функции, представляет собой список аргументов командной строки, которые должны быть переданы запускаемой программе. Формат этого списка должен быть таким же, как у списка argv[], который получает функция main(), то есть, первым элементом должно быть имя запущенной программы, а последним – нулевой указатель на строку, то есть

(char *) NULL

В последнем параметре функции execve() передается список переменных среды окружения формате

ИМЯ=ЗНАЧЕНИЕ

Массив переменных должен заканчиваться нулевым указателем на строку, как и argv. Копию набора переменных окружения, полученного вашей программой от оболочки, можно получить из внешней переменной environ, которую вы должны объявить в файле вашей программы:

extern char ** environ;

Передача списка переменных окружения явным образом позволяет вам, в случае необходимости, модифицировать список. В параметре argv, как и в параметре envp, можно передавать значения NULL, если вы уверены, что вызываемой программе не нужны переменные окружения или командная строка. Заголовок функции execvp() выглядит проще:

int execvp(const char *file, char *const argv[]);

Первый параметр функции, это имя запускаемого файла. Функция execvp() выполняет поиск имен с учетом значения переменной окружения PATH, так что для программы, которая расположена в одной из директорий, перечисленных в PATH, достаточно указывать одно лишь имя исполнимого файла. Второй параметр функции соответствует второму параметру execve(). У функции execvp() нет параметра для передачи набора переменных среды окружения, но это не значит, что запущенная программа не получит эти переменные. Программе будет передана копия набора переменных окружения родительского процесса. Вы можете изменить переменные среды окружения не только путем модификации списка environ, но и с помощью набора специальных функций. Для установки новой переменной окружения используется putenv(3). У функции putenv() один параметр типа char *. В этом параметре функции передается пара ИМЯ=ЗНАЧЕНИЕ для установки новой переменной. Функция возвращает 0 в случае успеха и -1 в случае ошибки. Программа может прочитать значение переменной окружения с помощью вызова getenv(3). У этой функции так же один параметр типа «строка с нулевым конечным символом». В этом параметре передается имя переменной, значение которой мы хотим прочитать. В случае успеха функция возвращает строку, содержащую значение переменной, а в случае неудачи (например, если запрошенной переменной не существует) – NULL.

Переменные среды окружения играют важную роль в работе процессов, поскольку многие системные функции используют их для получения различных параметров, необходимых для нормальной работы программ, однако, управление программами с помощью переменных окружения считается морально устаревшим методом. Если ваша программа должна получать какие-то данные извне, воздержитесь от создания новых переменных окружения. Современные программы чаще полагаются на файлы конфигурации, средства IPC и прикладные интерфейсы. Старайтесь так же не использовать переменные окружения для получения тех данных, которые вы можете получить с помощью специальных функций C, например, используйте getcwd(3) вместо обращения к переменной PWD. Полный список установленных переменных окружения и их значений вы можете получить с помощью команды env, а некоторые наиболее важные переменные приведены в таблице:

Переменная Описание
DISPLAY Имя дисплея X Window
HOME Полный путь к домашней директории пользователя-владельца процесса
HOST Имя локального узла
LANG Текущая локаль
LOGNAME Имя пользователя-владельца процесса
PATH Список директорий для поиска имен файлов
PWD Полный путь к текущей рабочей директории
SHELL Имя оболочки, заданной для пользователя-владельца процесса по умолчанию
TERM Терминал пользователя-владельца процесса по умолчанию
TMPDIR Полный путь к директории для хранения временных файлов
TZ Текущая временная зона

Помимо функций getenv() и putenv() есть еще несколько функций для работы с переменными среды окружения. Функция setenv() позволяет установить значение переменной. В отличие от функции putenv(), эта функция позволяет установить флаг, благодаря которому значение уже существующей переменной не будет изменено. С помощью функции unsetenv() вы можете удалить переменную окружения. Наконец, если переменные среды окружения вам надоели, вы можете воспользоваться функцией clearenv() для удаления всего списка переменных. Под удалением переменных окружения подразумевается их удаление из среды окружения текущего процесса и его потомков. Это удаление никак не повлияет на переменные среды окружения других процессов.

Рассмотрим использование функции execvp() на примере программы, запускающей другую программу, имя которой передается ей в качестве аргумента командной строки. Назовем нашу программу exec (ее исходные тексты вы найдете в исходниках программы в файле exec.c). Если вы скомпилируете файл exec.c и скомандуете, например

./exec ls -al

программа выполнит команду ls –al и возвратит сведения о том, как был завершен соответствующий процесс. Ниже приводится полный исходный текст exec.

#include <sys/types.h>#include <sys/wait.h>#include <stdlib.h>#include <stdio.h>#include <errno.h>int main(int argc, char * argv[]){ int pid, status;if (argc < 2) {printf("Usage: %s command, [arg1 [arg2]...]\n", argv[0]);return EXIT_FAILURE;}printf("Starting %s...\n", argv[1]);pid = fork();if (pid == 0) {execvp(argv[1], &argv[1]);perror("execvp");return EXIT_FAILURE; // Never get there normally} else {if (wait(&status) == -1) {perror("wait");return EXIT_FAILURE;}if (WIFEXITED(status))printf("Child terminated normally with exit code %i\n",WEXITSTATUS(status));if (WIFSIGNALED(status))printf("Child was terminated by a signal #%i\n", WTERMSIG(status));if (WCOREDUMP(status))printf("Child dumped core\n");if (WIFSTOPPED(status))printf("Child was stopped by a signal #%i\n", WSTOPSIG(status));}return EXIT_SUCCESS;}

Поскольку мы хотим, чтобы новая программа не заменяла старую, а выполнялась одновременно с ней, мы раздваиваем процесс с помощью fork(). В дочернем процессе, которому fork() возвращает 0, мы выполняем вызов execvp(). Первый параметр execvp() – значение argv[1], в котором должно быть передано имя запускаемой программы. В качестве второго параметра функции передается массив аргументов командной строки, полученных программой exec, начиная со второго элемента (элемент argv[1]). Например, если список аргументов программы exec имел вид "exec", "ls", "-al", то первым параметром функции execvp() будет строка "ls", а вторым параметром – массив из двух строк "ls" и "-al" (не забывайте, что первым элементом массива аргументов командной строки должно быть имя самой программы, по которому она была вызвана). Таким образом, вы можете, например, давать команды

./exec ls –al./exec./exec ls –al./exec./exec./exec ls –al

и так далее. Чего вы не можете, однако, сделать, так это выполнить с помощью нашей программы команду типа

./exec "ls > log.txt"

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

exec sh –c "ls –al > log.txt"

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

Обратимся теперь к родительскому процессу. Мы хотим, чтобы родительский процесс дождался завершения дочернего процесса и вывел сообщение о том, как был завершен дочерний процесс. Первую часть этой задачи мы могли бы решить с помощью уже известного нам механизма сигналов. Каждый раз, когда дочерний процесс приостанавливается или завершается, родительский процесс получает сигнал SIGCHLD. Мы могли бы приостановить работу программы до получения этого сигнала, но таким образом мы бы знали только то, что один из дочерних процессов программы завершился, но не знали бы, ни как он завершился, ни какой именно это был процесс. Причина этого конечно, в том, что сигналы сами по себе не несут никакой дополнительной информации. Однако, в нашем распоряжении есть функция wait(2), которая приостанавливает процесс до тех пор, пока один из его дочерних процессов не будет остановлен или не завершится, после чего возвращает информацию о том, какой процесс завершился и что стало причиной его завершения. Значение, возвращаемое функцией wait() – это PID завершившегося процесса, а аргументом функции должен быть указатель на переменную status типа int. В этой переменной функция вернет дополнительные сведения о том, как завершился процесс. Вы могли подумать, что после того как мы создали два процесса с помощью fork(), не так уж важно, запускаем ли мы новую программу в дочернем или в родительском процессе, ведь разница между ними невелика. Теперь вы знаете как минимум одну причину придерживаться строгих правил, ведь родительский процесс может следить за дочерним, в то время как обратное невозможно.

Значение переменной status, в которой функция wait() передает дополнительные данные о завершившемся процессе, представляет собой маску из нескольких разных параметров. В файле <sys/wait.h> определены макросы, упрощающие «расшифровку» этой маски. Макрос WIFEXITED возвращает значение 1, если процесс завершился «добровольно», то есть в результате вызова exit() или _exit(). В этом случае с помощью макроса WEXITSTATUS можно узнать код завершения, возвращенный процессом. Макрос WIFSIGNALED возвращает 1, если выполнение процесса было завершено сигналом. Номер этого сигнала можно узнать с помощью макроса WTERMSIG. Макрос WIFSTOPPED возвращает значение 1, если выполнение процесса было приостановлено сигналом, номер которого возвращает макрос WSTOPSIG.

Помимо функции wait() в нашем распоряжении есть еще две функции, позволяющие приостановить процесс в ожидании изменения состояния дочерней программы. Это функции waitpid(2) и waitid(2). Функция waitpid() может приостановить процесс в ожидании изменения состояния определенного дочернего процесса, заданного значением PID. У функции waitpid() три параметра. Первый параметр – значение PID процесса, завершения которого ждет функция. Если передать в этом параметре значение -1, функция будет ждать изменения состояния любого процесса, аналогично wait(). Если первый параметр равен нулю, waitpid() ждет завершения любого процесса из той же группы, что и текущий. Второй параметр waitpid() аналогичен параметру wait(). Третий параметр позволяет указать дополнительные флаги функции. Например, если установить флаг NOHANG, функция вернет управление немедленно, даже если ни один дочерний процесс не завершился (подробнее об этом будет сказано ниже). Результатом функции waitpid(), также как и в случае wait(), является идентификатор завершившегося процесса. Функция waitid(), появившаяся в Linux начиная с версии ядра 2.6.9, позволяет установить более тонкий контроль над параметрами ожидаемого события и получить более подробную информацию о сигнале, вызвавшем изменение состояния дочернего процесса. Для большинства задач возможности этой функции явно избыточны.

Исследуя работу программы exec, мы еще не коснулись одного важного момента, о котором часто забывают при отладке программ, создающих несколько процессов. Неявно мы все время предполагали, что вызов wait() в родительском процессе произойдет до завершения дочернего процесса. Чаще всего так и будет. Но что произойдет, если дочерний процесс завершится раньше вызова wait() в родительском процессе, например, вследствие ошибки при вызове execvp()?

Программируя несколько потоков или процессов, мы всегда должны учитывать подобные варианты развития событий. В нашем конкретном случае беспокоиться не о чем. Если дочерний процесс завершится до завершения родительского процесса, система сохранит его «след», именуемый зомби. Зомби будет существовать до тех пор, пока его родительский процесс не вызовет wait() (waitpid(), waitid()), или пока родительский процесс не завершится. Система сохраняет зомби процессов специально для того, чтобы родительский процесс мог исследовать причины завершения дочернего процесса, однако это не всегда удобно. Рассмотрим, например, процесс init, который является «папой» всех пользовательских процессов сеанса Linux. Поскольку init существует на протяжении всего сеанса, любой из его дочерних процессов по завершении оставался бы в виде зомби до тех пор, пока init не вызвал бы одну из функций wait*. Еще один процесс, который может породить множество зомби, это демон... Иногда я должен напоминать себе, что пишу статью по программированию, а не сценарий фильма ужасов. Для того, чтобы процесс не оставлял зомби, можно вызывать функцию waitpid() периодически (с флагом NOHANG, чтобы она не блокировала вызвавший процесс если в системе нет зомби). Можно поступить и иначе, назначив сигналу SIGCHLD обработчик, в котором вызывалась бы функция wait(). Именно так поступает программа nozombies, которую вы найдете в исходниках

int main(int argc, char * argv[]){ int i, pid;struct sigaction sa;sa.sa_handler = child_handler;sigaction(SIGCHLD, &sa, 0);for (i = 0; i < 10; i++) {pid = fork();if (pid == 0) {printf("I will leave no zombie\n");exit(0);}elseprintf("Created a process with the PID %i\n", pid);}while (1)sleep(1);return EXIT_SUCCESS;}

Программа nozombies устанавливает обработчик сигнала SIGCHLD (функция

child_handler()):void child_handler(int i){ int status;wait(&status);}

Далее программа создает несколько дочерних процессов, каждый из которых выводит диагностическое сообщение и завершает свою работу. Затем программа nozombies переходит в бесконечный цикл (так что завершать ее придется с помощью Ctrl-C). Пока программа находится в бесконечном цикле, вы можете проверить, оставила ли она процессы-зомби. Для этого откройте другой терминал и скомандуйте

ps –al

Для сравнения, закомментируйте строку

wait(&status);

в теле функции child_handler(), перекомпилируйте и снова запустите программу. Теперь команда ps –al покажет наличие десяти зомби-процессов (они помечены символом Z).

Интерфейс функций exec* может показаться слишком сложным и, для многих задач, избыточным. Стандарт языка C включает описание функции system(), предназначенной для запуска внешних программ. Единственный аргумент этой функции – строка запуска программы. Поскольку для выполнения программы функция system() запускает копию оболочки (той, которая в вашей системе вызывается командой sh(1)), эта функция выполнит любую команду, которую вы могли бы ввести в командной строке оболочки. Функция system() (пример ее использования вы найдете в файле system.c) приостанавливает выполнение вызвавшей ее программы до тех пор, пока дочерний процесс не завершит работу и возвращает код завершения процесса.

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

 

 

Процессы в Linux

Filed under: Введение, Теория — gn_serg @ 07:30

В жизни обычного пользователя Linux часто встречается термин «процессы». Так что же такое «процесс»? Попробуем разобраться.

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

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

Работающий процесс – в данный момент код процесса выполняется.

Спящий процесс – в данный момент код процесса не выполняется в ожидании какого либо события (нажатия клавиши на клавиатуре, поступление данных из сети и т.д.)

Процесс-зомби – сам процесс уже не существует, его код и данные выгружены из оперативной памяти, но запись в таблице процессов остается по тем или иным причинам.

Каждому процессу в системе назначаются числовые идентификаторы (личные номера) в диапазоне от 1 до 65535 (PID – Process Identifier – идентификатор процесса) и идентификаторы родительского процесса (PPID – Parent Process Identifier – идентификатор родительского процесса). PID является именем процесса, по которому мы можем адресовать процесс в операционной системе при использовании различных средств просмотра и управления процессами. PPID определяет родственные отношения между процессами, которые в значительной степени определяют его свойства и возможности. Другие параметры, которые необходимы для работы программы, называют “окружение процесса”. Одним из таких параметров является управляющий терминал – имя терминального устройства, на которое процесс выводит информацию и с которого информацию получает. Управляющий терминал имеют далеко не все процессы. Процессы, не привязанные к какому-то конкретному терминалу называются “демонами” (daemons). Такие процессы, будучи запущенными пользователем, не завершают свою работу по окончании сеанса, а продолжают работать, так как они не связаны никак с текущим сеансом и не могут быть автоматически завершены. Как правило, с помощью демонов реализуются серверные службы, так например сервер печати реализован процессом-демоном cupsd, а сервер журналирования – syslogd.

Для просмотра списка процессов в Linux существует команда ps. Формат команды следующий:

ps [PID] [options] – просмотр списка процессов. Без параметров ps показывает все процессы, которы были запущены в течение текущей сессии, за исключением демонов. Options может принимать одно из следующих значений или их комбинации:

-а или -e – показать все процессы

-f – полный листинг

-w – показать полные строки описания процессов. Если они превосходят
длину экрана, то перенести описание на следующую строку.

Это далеко не все параметры команды ps. Остальные параметры Вы можете узнать, просто набрав man ps.

Пример1:

[gserg@WEBMEDIA gserg]$ ps

PID TTY TIME CMD

3126 pts/2 00:00:00 bash

3158 pts/2 00:00:00 ps

[gserg@WEBMEDIA gserg]$_

 

Пример2:

[gserg@WEBMEDIA gserg]$ ps 3126

PID TTY STAT TIME COMMAND

3126 pts/2 S 0:00 /bin/bash

[gserg@WEBMEDIA gserg]$_

 

Пример3:

[gserg@WEBMEDIA gserg]$ ps -ef

UID PID PPID C STIME TTY TIME CMD

root 1 0 0 10:01? 00:00:03 init [5]

root 2 1 0 10:01? 00:00:00 [keventd]

root 3 1 0 10:01? 00:00:00 [kapmd]

root 4 1 0 10:01? 00:00:00 [ksoftirqd_CPU0]

root 5 1 0 10:01? 00:00:24 [kswapd]

root 6 1 0 10:01? 00:00:00 [bdflush]

gserg 3126 3124 0 17:56 pts/2 00:00:00 /bin/bash

gserg 3160 3126 0 17:59 pts/2 00:00:00 ps -ef

[gserg@WEBMEDIA gserg]$_

 

Пример4:

[gserg@WEBMEDIA gserg]$ ps -efw

UID PID PPID C STIME TTY TIME CMD

root 1 0 0 10:01? 00:00:03 init [5]

root 2 1 0 10:01? 00:00:00 [keventd]

root 3 1 0 10:01? 00:00:00 [kapmd]

root 4 1 0 10:01? 00:00:00 [ksoftirqd_CPU0]

root 5 1 0 10:01? 00:00:24 [kswapd]

……

root 1130 1 0 10:02? 00:00:00 /usr/sbin/apmd -p 10 -w 5 -W -P /etc/sysconfig/apm-scripts/apmd_proxy

gserg 3172 3126 0 18:01 pts/2 00:00:00 ps -efw

[gserg@WEBMEDIA gserg]$_

 

Процессы в ОС Linux обладают теми же правами, которыми обладает пользователь, от чьего имени был запущен процесс.

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

Для определения имени пользователя, запустившего процесс, операционная система использует реальные идентификаторы пользователя и группы, назначаемые процессу. Но эти идентификаторы не являются решающими при определении прав доступа. Для этого у каждого процесса существует другая группа идентификаторов – эффективные.

Как правило, реальные и эффективные идентификаторы процессов одинаковые, но есть и исключения. Например, для работы утилиты passwd необходимо использовать идентификатор суперпользователя, так как только суперпользователь имеет права на запись в файлы паролей. В этом случае эффективные идентификаторы процесса будут отличаться от реальных. Возникает резонный вопрос – как это было реализовано?

У каждого файла есть набор специальных прав доступа – биты SUID и SGID. Эти биты позволяют при запуске программы присвоить ей эффективные идентификаторы владельца и группы-владельца соответственно и выполнять процесс с правами доступа другого пользователя. Так как файл passwd принадлежит пользователю root и у него установлен бит SUID, то при запуске процесс passwd будет обладать правами пользователя root.

Устанавливаются биты SGID и SUID командой chmod:

chmod u+s filename – установка бита SUID

chmod g+s filename – установка бита SGID

 

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

Родителем всех процессов в системе является процесс init. Его PID всегда 1, PPID – 0. Всю таблицу процессов можно представить себе в виде дерева, в котором корнем будет процесс init. Этот процесс хоть и не является частью ядра, но выполняет в системе очень важную роль – определяет текущий уровень инициализации системы и следит чтобы были запущены программы, позволяющие пользователю общаться с компьютером (mingetty, X или другие).

Процессы, имена которых заключены в квадратные скобки, например “[keventd]” – это процессы ядра. Эти процессы управляют работой системы, а точнее такими ее частями, как менеджер памяти, планировщик времени процессора, менеджеры внешних устройств и так далее.

Остальные процессы являются пользовательскими, запущенными либо из командной строки, либо во время инициализации системы.

Жизнь каждого процесса представлена следующими фазами:

Создание процесса – на этом этапе создается полная копия того процесса, который создает новый. Например, вы запустили из интерпретатора на выполнение команду ls. Командный интерпретатор создает свою полную копию.

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






Не нашли, что искали? Воспользуйтесь поиском:

vikidalka.ru - 2015-2024 год. Все права принадлежат их авторам! Нарушение авторских прав | Нарушение персональных данных