А вот с кодом возврата этой функции в случае удачи сложнее и гораздо интереснее. Дело в том, что для одного вызова
fork()
одновременно имеют место два возврата в двух различных копиях (но в текстуально едином коде!): в копии кода, соответствующей дочернему процессу, возвращается 0, а в копии кода родителя — PID успешно порожденного дочернего процесса. Это и позволяет разграничить в едином программном коде фрагменты, которые после точки ветвления надлежит выполнять в родительском процессе и которые относятся к дочернему процессу. Типичный шаблон кода, использующего
fork()
, выглядит примерно так:
pid_t pid = fork();
if (pid == -1) perror("fork"), exit(EXIT_FAILURE);
if (pid == 0) {
// ... этот код выполняется в дочернем процессе ...
exit(EXIT_SUCCESS);
}
if (pid > 0) {
// ... этот код выполняется в родительском процессе ...
do { // ожидание завершения порожденного процесса
wpid = waitpid(pid, &status, 0);
} while(WIFEXITED(status) == 0);
exit(WEXITSTATUS(status));
}
Эта схема порождения процесса, его клонирование, настолько широко употребляется, особенно при построении самых разнообразных серверов, что для нее была создана специальная техника, построенная на вызове
fork()
. Заметьте, что во всех многозадачных ОС обязательно присутствует та или иная техника программного создания нового процесса, однако не во всех существует техника именно клонирования, то есть создания полного дубликата порождающего процесса.
Вот как выглядит простейший ретранслирующий TCP/IP-сервер, заимствованный из нашей более ранней публикации [4] (обработка ошибок полностью исключена, чтобы упростить пример):
int main(int argc, char* argv[]) {
// создание и подготовка прослушивающего сокета:
int rc, ls = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &rc, sizeof(rc));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr); // специфика QNX
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT); // PORT - константа
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(ls, (struct sockaddr*)&addr, sizeof(sockaddr));
listen(ls, 25);
while(true) {
rc = accept(ls, NULL, NULL);
pid_t pid = fork();
if (pid < 0) ...; // что-то произошло!
if (pid == 0) {
close(ls);
char data[MAXLINE];
int nd = read(rc, &data, MAXLINE);
if (nd > 0) write(rc, &data, nd);
close(rs);
exit(EXIT_SUCCESS);
}
else close(rs); // единственное действие родителя
}
exit(EXIT_SUCCESS);
}
Приведенный фрагмент может в процессе своей работы породить достаточно много идентичных процессов (один родительский, пассивно прослушивающий канал; остальные — порожденные, активно взаимодействующие с клиентами, по одному на каждого клиента). Все порождаемые процессы наследуют весь набор дескрипторов (в данном случае сокетов), доступных родительскому процессу. Лучшее, что могут сделать процессы (как родительский, так и дочерний), — немедленно после вызова
fork()
(и это хорошая практика в общем случае) тщательно закрыть все унаследованные дескрипторы, не имеющие отношения к их работе.