Здесь приведен перевод главы 2 из FAQ по программированию Linux [1], посвященной управлению файлами, включая каналы (pipes) и сокеты (sockets). См. также Sockets FAQ [2]. Описание незнакомых терминов и аббревиатур см. в Словарике статьи [3].
[Список вопросов]
2. Общие принципы работы с файлами (включая pipe и socket)
2.1 Как обслуживать несколько соединений?
2.1.1 Как использовать select()?
2.1.2 Как использовать poll()?
2.1.3 Можно ли использовать SysV IPC одновременно с select или poll?
2.2 Как узнать, что отключился другой конец соединения?
2.3 Как лучше всего читать директории?
2.4 Как узнать, открыл ли файл кто-то еще?
2.5 Как получить привилегированный доступ к файлу ('lock' file)?
2.6 Как определить, что файл был обновлен другим процессом?
2.7 Как работает утилита du?
2.8 Как определить размер файла?
2.9 Как развернуть символ домашней директории '~' в полный путь до файла, как это делает шелл?
2.10 Что можно делать с именованными каналами (named pipes, FIFO)?
2.10.1 Что такое named pipe?
2.10.2 Как создать named pipe?
2.10.3 Как использовать a named pipe?
2.10.4 Можно ли использовать named pipe в NFS?
2.10.5 Могут ли несколько процессов одновременно записывать данные в pipe?
2.10.6 Использование именованных каналов в приложениях
2.1 Как обслуживать несколько соединений? =========================================
Я должен мониторить одновременно больше одного соединения (fd/connection/stream). Как управлять ими всеми?
Используйте select() или poll().
Замечание: select() была представлена в BSD, в то время как poll() это артефакт SysV STREAMS. В результате возможны проблемы с портируемостью; в чистых системах BSD может отсутствовать poll(), в то время как в некоторых старых системах SVR3 может не быть select(). SVR4 добавила select(), и стандарт Posix.1g определяет оба этих метода.
Вызовы select() и poll() по сути делают одно и то же, но по-разному. Они оба проверяют набор дескрипторов файла, чтобы определить, есть ли определенные события ожидания на каком-либо из них, и затем опционально ждут указанное время возникновения указанного события.
Важное замечание: ни select(), ни poll() не делают что-нибудь полезное, когда применяются к обычным файлам; их применение имеет смысл для сокетов (socket), каналов (pipe), консолей управления (pty, tty) и возможно для других символьных устройств, однако это зависит от системы.
На этом подобие этих методов заканчивается...
2.1.1 Как использовать select()? --------------------------------
Интерфейс select() в основан главным образом на концепции fd_set, что представляет собой набор дескрипторов файлов, FD (обычно реализованный как bit-vector). В прошлом было принято считать, что FD были меньше 32, и просто использовалось int для хранения набора, но сейчас обычно доступно большее количество FD, так что важно использовать стандартные макросы для манипуляций над fd_set:
fd_set set;
FD_ZERO(&set); /* опустошает набор */
FD_SET(fd,&set); /* добавляет FD к набору */
FD_CLR(fd,&set); /* удаляет FD из набора */
FD_ISSET(fd,&set) /* true, если FD присутствует в наборе */
В большинстве случаев система отвечает за то, чтобы обрабатывался весь диапазон набора дескрипторов файлов FD, однако в некоторых случаях вы должны предварительно определить макрос FD_SETSIZE. Этот макрос СИСТЕМНО-зависимый, проверьте описание manpage для вашего select(). Также у некоторых систем есть проблемы с обработкой больше чем 1024 файловых дескрипторов в select().
Базовый интерфейс функции select простой:
int select(int nfds, fd_set *readset,
fd_set *writeset,
fd_set *exceptset,
struct timeval *timeout);
Функция select принимает следующие параметры:
nfds количество FD для проверки; это значение должно быть больше, чем самый большой FD в любом из наборов fd_set, НО НЕ РЕАЛЬНОЕ указанное количество FD.
readset набор FD для проверки на возможность чтения.
writeset набор FD для проверки на возможность записи.
exceptfds набор FD для проверки на статус исключений (замечание: ошибки НЕ ОТНОСЯТСЯ к состояниям исключения)
timeout NULL для бесконечного таймаута, либо здесь передается указатель, задающий максимальное время ожидания (если tv_sec и tv_usec оба равны 0, то статус FD опрашивается, но блокировка никогда не происходит).
Вызов возвратит количество найденных находящихся в готовности (ready) FD, и три fd_set модифицируются по месту, в наборах остаются только ready FD. Используйте макрос FD_ISSET для проверки возвращенных наборов.
Вот пример тестирования одного FD на возможность чтения:
int isready(int fd)
{
int rc;
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd,&fds);
tv.tv_sec = tv.tv_usec = 0;
rc = select(fd+1, &fds, NULL, NULL, &tv);
if (rc < 0)
return -1;
return FD_ISSET(fd,&fds) ? 1 : 0;
}
Обратите внимание, что мы можем передать NULL для тех fd_set, которые нас не интересуют в плане проверки.
2.1.2 Как использовать poll()? ------------------------------
Функция poll() принимает указатель на список структур опрашиваемых дескрипторов pollfd, в которых сохранены дескрипторы и события, которые вы хотите опрашивать. События (events) указываются через битовую маску в поле events этой структуры. Экземпляр структуры будет позже заполнен и возвращен при любом возникшем событии. Макрос, определенный poll.h на SVR4 (и возможно на более старых версиях), используется для указания событий в этом поле. Таймаут может быть указан в миллисекундах, только предоставленный тип указывается как int, что вызывает некоторое недоумение. Таймаут 0 приведет к тому, что poll() выполнит немедленный возврат; значение -1 приведет к тому, что poll приостановит выполнение вызвавшего потока, пока не возникнет событие.
struct pollfd {
int fd; /* Дескриптор файла. */
short events; /* Здесь указывается событие (события). */
short revents; /* Здесь возвращаются найденные события. */
};
Наподобие select(), если возвращаемое значение положительное, то оно отражает, сколько дескрипторов было найдено для удовлетворения запрошенных событий. Нулевое возвращенное значение показывает, что был достигнут таймаут, и никакое их указанных событий не произошло. Если возвращено отрицательное значение, то немедленно должна последовать проверка errno, поскольку это сигнал об ошибке.
Если не найдено ни одно событие, что revents очищается, так что это не нужно делать самостоятельно.
Возвращаемые события проверяются на предмет определения событий. Пример:
/* Опрос двух дескрипторов для обычных данных (NORMAL_DATA),
или высокоприоритетных данных (HIPRI_DATA). Если что-то
обнаружено, то вызывается функция handle() с соответствующим
дескриптором и приоритетом. Не превышайте время ожидания,
сдавайтесь только в случае ошибки или зависания одного
из дескрипторов. */
#include < stdlib.h>
#include < stdio.h>
#include < sys/types.h>
#include < stropts.h>
#include < poll.h>
#include < unistd.h>
#include < errno.h>
#include < string.h>
#define NORMAL_DATA 1
#define HIPRI_DATA 2
int poll_two_normal(int fd1,int fd2)
{
struct pollfd poll_list[2];
int retval;
poll_list[0].fd = fd1;
poll_list[1].fd = fd2;
poll_list[0].events = POLLIN|POLLPRI;
poll_list[1].events = POLLIN|POLLPRI;
while(1)
{
retval = poll(poll_list,(unsigned long)2,-1);
/* В этом случае retval будет всегда больше 0, или будет -1,
поскольку мы делаем это с использованием блокировки. */
if(retval < 0)
{
fprintf(stderr,"Error while polling: %s\n",strerror(errno));
return -1;
}
if(((poll_list[0].revents&POLLHUP) == POLLHUP) ||
((poll_list[0].revents&POLLERR) == POLLERR) ||
((poll_list[0].revents&POLLNVAL) == POLLNVAL) ||
((poll_list[1].revents&POLLHUP) == POLLHUP) ||
((poll_list[1].revents&POLLERR) == POLLERR) ||
((poll_list[1].revents&POLLNVAL) == POLLNVAL))
return 0;
if((poll_list[0].revents&POLLIN) == POLLIN)
handle(poll_list[0].fd,NORMAL_DATA);
if((poll_list[0].revents&POLLPRI) == POLLPRI)
handle(poll_list[0].fd,HIPRI_DATA);
if((poll_list[1].revents&POLLIN) == POLLIN)
handle(poll_list[1].fd,NORMAL_DATA);
if((poll_list[1].revents&POLLPRI) == POLLPRI)
handle(poll_list[1].fd,HIPRI_DATA);
}
}
2.1.3 Можно ли использовать SysV IPC одновременно с select или poll? --------------------------------------------------------------------
НЕТ. Исключение составляет AIX, где существует невероятный костыль кода, позволяющий это сделать.
Как правило попытки совместить select() или poll() с использований очередей сообщений SysV вызывают затруднения. Объекты SysV IPC не обрабатываются дескрипторами файла, так что они не могут быть переданы в select() или poll(). Существует несколько способов обойти проблему разной степени уродства:
- Полный отказ от SysV IPC. :-) - fork(), и обработка в дочернем процессе SysV IPC, реализация обмена с родительским процессом через pipe или socket, где родительский процесс может запустить select(). - То же самое, что и предыдущий способ, но здесь делает select() дочерний процесс, и обмен с родительским процессом происходит через очередь сообщений. - Организовать процесс, который отправляет вам сигнал после каждого сообщения. ПРЕДУПРЕЖДЕНИЕ: правильная обработка этого способа не тривиальна; здесь очень легко написать код, который потенциально может терять сообщения и входить в бесконечную самоблокировку (deadlock).
Примечание: есть и другие методы.
2.2 Как узнать, что отключился другой конец соединения? =======================================================
Если вы пытаетесь прочитать из канала (pipe), сокета (socket), FIFO и т. д., когда соединение на другом конце закрыто, то вы получите индикацию о конце файла (end-of-file, EOF, т. е. read() возвратит 0 прочитанных байт). Если вы попытаетесь записать в канал, сокет и т. д., когда соединение на противоположной стороне закроется, то процессу будет передан сигнал SIGPIPE, убивая процесс, если этот сигнал не был перехвачен (если вы проигнорируете или заблокируете сигнал, то вызов write() завершится неудачно с ошибкой EPIPE).
2.3 Как лучше всего читать директории? ======================================
Хотя исторически для этого есть несколько разных интерфейсов, в наши дни имеет значение единственный интерфейс стандарта Posix.1, это функции < dirent.h>.
Функция opendir() откроет указанную директорию; readdir() читает элементы директории в стандартизованном формате; назначение closedir() очевидно. Также предоставляются понятно для чего нужные функции rewinddir(), telldir() и seekdir().
Если вы ищете раскрытие имени файла для wildcard, то на большинстве систем есть функция glob(); также проверьте, доступна ли fnmatch(), которая ищет совпадение имен файлов с wildcard, или ftw() для просмотра всего дерева директории.
2.4 Как узнать, открыл ли файл кто-то еще? ==========================================
Это еще один кандидат на "вопросы без ответа", потому что в общем случае вашей программе не должно быть интересно, открыл ли кто-то еще какой-либо файл. Если вам нужно иметь дело с конкурентным доступом к файлу, то следует смотреть в сторону рекомендательной блокировки (advisory locking).
Так или иначе это все довольно сложно для реализации. Такие утилиты, как fuser и lsof, которые узнают об открытых файлах, делают это путем просмотра данных ядра весьма нездоровым способом. Также вы не сможете с гарантированной пользой вызвать их из своей программы, потому что когда вы узнаете, что какой-то файл открыт (или закрыт), эта информация может стать уже устаревшей.
2.5 Как получить привилегированный доступ к файлу ('lock' file)? ================================================================
Существует 3 основных механизма блокировки файла. Все они рекомендуются к использованию*, т. е. они полагаются на программы, сотрудничающие друг с другом при выполнении своей работы. Поэтому крайне важно, чтобы все программы в приложении были последовательны в своем режиме блокировки, и требуется большая осторожность, когда ваши программы могут совместно использовать файлы со сторонним ПО.
Примечание (*): на самом деле некоторые Unix-ы позволяют делать обязательную блокировку через бит sgid, RTFM для описания этого хака.
Некоторые приложения используют блокировку файлов - иногда что-то наподобие FILENAME.lock. Однако простая проверка на предмет существования таких файлов не будет адекватной, потому что процесс может быть убит после того, как заказал блокировку файла. Метод, который использует UUCP (вероятно это самый заметный пример: блокировка файлов используется для управления доступом к модемам, удаленным системам и т. п.), заключается в сохранении PID в файле блокировки, и проверки, работает ли еще этот pid. Даже этого недостаточно для полной уверенности (поскольку PID-ы используются повторно); должна быть обратная проверка, чтобы узнать, что файл блокировки устарел, т. е. процесс, удерживающий блокировку, должен регулярно обновлять файл. Грязный метод.
Существуют следующие функции блокировки:
flock(); lockf(); fcntl();
Функция flock() пришла из BSD, и она доступна на большинстве (но не на всех) Unix-ах. Она простая и эффективная на одном хосте, но по сути не работает вместе с NFS. Функция блокирует весь файл. Популярный язык программирования Perl реализует свою собственную flock(), что довольно обманчиво создает иллюзию полной переносимости.
Функция fcntl() предоставляет только POSIX-совместимый механизм блокировки, и поэтому является единственной действительно портируемой блокировкой. Также она самая мощная и самая сложная для использования. Для файловых систем, смонтированных на NFS, запросы fcntl() передаются демону (rpc.lockd), который общается с lockd на хосте сервера. В отличие от flock(), функция fcntl() может делать блокировку на уровне записи.
Функция lockf() это просто упрощенный интерфейс к блокирующим функциям fcntl().
Какой бы механизм блокировки вы ни использовали, важно синхронизировать все ваши файловые операции ввода-вывода, пока блокировка активна:
lock(fd);
write_to(some_function_of(fd));
flush_output_to(fd); /* НИКОГДА не делайте unlock, пока вывод может быть буферизирован */
unlock(fd);
do_something_else; /* другой процесс может обновить файл */
lock(fd);
seek(fd, somewhere); /* поскольку наш старый указатель по файлу небезопасен */
do_something_with(fd);
...
Вот несколько полезных рецептов блокировки fcntl() (для упрощения обработка ошибок опущена):
#include < fcntl.h>
#include < unistd.h>
read_lock(int fd) /* shared-блокировка всего файла */
{
fcntl(fd, F_SETLKW, file_lock(F_RDLCK, SEEK_SET));
}
write_lock(int fd) /* исключительная блокировка всего файла */
{
fcntl(fd, F_SETLKW, file_lock(F_WRLCK, SEEK_SET));
}
append_lock(int fd) /* блокировка на конце файла (EOF) - другие
процессы могут обращаться к существующим
записям в файле */
{
fcntl(fd, F_SETLKW, file_lock(F_WRLCK, SEEK_END));
}
Используемая выше функция file_lock:
struct flock* file_lock(short type, short whence)
{
static struct flock ret;
ret.l_type = type;
ret.l_start = 0;
ret.l_whence = whence;
ret.l_len = 0;
ret.l_pid = getpid();
return &ret;
}
2.6 Как определить, что файл был обновлен другим процессом? ===========================================================
Этот вопрос близок к качеству "частые вопросы без ответа", потому что те, кто это спрашивает, часто ищут какое-то уведомление от системы при изменении файла или каталога, и нет для этого портируемого метода (в IRIX есть нестандартный функционал для мониторинга доступа к файлу, однако автор не слышал о доступности такого функционала для других версий систем).
В общем случае лучше всего определить стороннее обновление файла - использовать на нем fstat() (замечание: накладные расходы на использование fstat() довольно низкие, обычно намного ниже чем накладные расходы stat()). Путем анализа mtime и ctime файла вы можете обнаружить его модификацию, или его события deleted/linked/renamed. Это несколько топорный способ, так что вероятно следует переосмыслить, ПОЧЕМУ вы хотите так делать.
2.7 Как работает утилита du? ============================
Программа du просто пересекает структуру каталогов, вызывая stat() (или точнее lstat()) на каждом файле и каталоге, которые встречаются, суммируя количество блоков, потребляемых каждым элементом.
Если вы хотите узнать подробности, как это работает, то смотрите исходный код системы. Исходный код систем семейства BSD (FreeBSD, NetBSD и OpenBSD) доступен как распакованное дерево кода на их FTP-сайтах дистрибуции; исходники для GNU-версий утилит доступен на любом из зеркал GNU, однако распаковывать файлы придется самостоятельно.
2.8 Как определить размер файла? ================================
Если у вас файл открыт, то используйте stat() или fstat(). Эти вызовы заполнят структуру данных, которая содержит всю информацию о файле, которую отслеживает файловая система. Это включает плава доступа (owner, group, permissions), размер, последнее время доступа, время последней модификации и т. д.
Следующая функция показывает, как использовать stat() для определения размера файла.
#include < stdlib.h>
#include < stdio.h>
#include < sys/types.h>
#include < sys/stat.h>
int get_file_size(char *path, off_t *size)
{
struct stat file_stats;
if(stat(path,&file_stats))
return -1;
*size = file_stats.st_size;
return 0;
}
Другой способ - открыть файл на чтение и использовать последовательность вызовов fseek() и ftell(). С помощью fseek прокручивают позицию чтения файла в его конец (передачей параметра SEEK_END), а функция ftell возвратит конец файла.
int get_file_size (char *path, off_t *size)
{
int result = -1;
FILE *fp = NULL;
do
{
fp = fopen(path, "r");
if (fp == NULL)
{
printf("failed to fopen %s\n", path);
break;
}
if (fseek(fp, 0, SEEK_END) == -1)
{
printf("failed to fseek %s\n", path);
break;
}
*size = ftell(fp);
result = 0;
} while (false);
if (fp)
fclose(fp);
return result;
}
2.9 Как развернуть символ домашней директории '~' в полный путь до файла, как это делает шелл? ==============================================================================================
Стандартная интерпретация символа '~' (тильда) в начале имени файла следующая: если это один символ, или за ним идет '/', то заменить его на домашнюю директорию текущего пользователя; если за ним идет имя пользователя, то заменить это на домашнюю директорию этого пользователя. Если не найден ни один из этих вариантов расширения, то шелл оставит имя файла не измененным.
Однако следует опасаться имен файлов, которые начинаются на символ '~'. Не избирательное расширение тильдой может очень затруднить указание таких имен файлов для программы; хотя заключение в кавычки предотвратит для шелл преобразование символа '~', кавычки будут удалены к моменту, когда программа увидит имя файла. Принимайте в качестве основного правила, что не надо пытаться выполнить преобразование '~' на именах файлов, которые передаются программе через командную строку или переменные окружения. В других случаях, когда имена файлов генерируются в самой программе, когда имя вводит пользователь, или когда имена файлов извлекаются из конфигурационного файла, вполне разумно применять расширение тильды.
Вот кусок кода C++ (используется стандартный класс string), который делает эту работу:
string expand_path(const string& path)
{
if (path.length() == 0 || path[0] != '~')
return path;
const char *pfx = NULL;
string::size_type pos = path.find_first_of('/');
if (path.length() == 1 || pos == 1)
{
pfx = getenv("HOME");
if (!pfx)
{
// Удар с лета. Мы пытаемся расширить ~/, однако HOME не установлена.
struct passwd *pw = getpwuid(getuid());
if (pw)
pfx = pw->pw_dir;
}
}
else
{
string user(path,1,(pos==string::npos) ? string::npos : pos-1);
struct passwd *pw = getpwnam(user.c_str());
if (pw)
pfx = pw->pw_dir;
}
// Если у нас не получилось выполнить расширение тильды, то будет возвращен
// неизменный путь path.
if (!pfx)
return path;
string result(pfx);
if (pos == string::npos)
return result;
if (result.length() == 0 || result[result.length()-1] != '/')
result += '/';
result += path.substr(pos+1);
return result;
}
2.10 Что можно делать с именованными каналами (named pipes, FIFO)? ==================================================================
2.10.1 Что такое named pipe? ----------------------------
Именованный канал (named pipe) это специальный файл, который используется для передачи данных между несвязанными процессами. Один процесс (или несколько процессов) записывает в канал, в то время как другой процесс читает из канала. Именованные каналы видны в файловой системе, и их можно просмотреть с помощью утилиты ls так же, как любой другой файл. Именованные каналы также называют стандартным термином FIFO (First In First Out, т. е. первым вошел первым вышел).
Именованные каналы могут использоваться для передачи данных между не связанными процессами, в то время как нормальные, не именованные каналы (unnamed pipe) могут соединять друг с другом только родственные (parent/child) процессы.
Именованные каналы строго однонаправленные даже на системах, где есть двунаправленные (full-duplex) анонимные каналы (anonymous pipe).
2.10.2 Как создать named pipe? ------------------------------
Чтобы интерактивно создать именованный канал, вы будете использовать либо mknod, либо mkfifo. На некоторых системах mknod находится в /etc. Другими словами, это может не быть в ваших настроенных путях поиска запускаемых файлов. Подробности см. в man-документации.
Пример программы на языке C, создающей именованный канал с помощью mkfifo():
/* явная установка umask, вы не знаете, где она была */
umask(0);
if (mkfifo("test_fifo", S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP))
{
perror("mkfifo");
exit(1);
}
Если у вас нет mkfifo(), то нужно использовать mknod():
/* явная установка umask, вы не знаете, где она была */
umask(0);
if (mknod("test_fifo",
S_IFIFO | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP,
0))
{
perror("mknod");
exit(1);
}
2.10.3 Как использовать a named pipe? -------------------------------------
Чтобы использовать канал, вы открываете его как обычный файл, и затем используйте read() и write(), как если бы это был обычный канал.
Однако open() на канале может вызвать блокировку. Применяются следующие правила:
* Если вы открыли канал одновременно на чтение и запись ('O_RDWR'), то open не вызовет блокировку. * Если вы сделали open на чтение ('O_RDONLY'), то open заблокирует вызвавший процесс, пока другой процесс открыл FIFO для записи, за исключением случая, когда указан O_NONBLOCK, тогда open нормально выполнится. * Если вы откроете канал на запись O_WRONLY, то open заблокирует вызвавший процесс, пока другой процесс открыл FIFO для чтения, за исключением случая, когда указан O_NONBLOCK, в этом случае вызов open будет неудачным.
Когда происходит чтение и запись FIFO, следует иметь в виду некоторые особенности для обычных каналов и сокетов, т. е. 'read() вернет EOF, когда все записывающие потоки закрыты, и write() приведет к возникновению SIGPIPE, когда нет ни одного читающего потока. Если сигнал SIGPIPE блокируется или игнорируется, то вызов завершится неудачей с ошибкой EPIPE.
2.10.4 Можно ли использовать named pipe в NFS? ----------------------------------------------
Нельзя, в протоколе NFS такой функционал не предусмотрен. Хотя вы можете использовать именованный канал на NFS-mounted файловой системе, чтобы осуществить обмен данными на одном и том же клиенте.
2.10.5 Могут ли несколько процессов одновременно записывать данные в pipe? --------------------------------------------------------------------------
Если размер каждой порции данных, записываемой в канал, меньше PIPE_BUF, то они не будут чередоваться. Однако границы записей не сохраняются; когда вы читаете из pipe, вызов read вернет столько данных, сколько возможно, даже если данные поступили от разных случаев записи.
Значение PIPE_BUF гарантировано (стандартом Posix) не меньше 512. Оно может быть определено, или может быть не определено в < limits.h>, однако может быть запрошено индивидуально для каналов с помощью pathconf() или fpathconf().
2.10.6 Использование именованных каналов в приложениях ------------------------------------------------------
Как можно реализовать двусторонний обмен между одним сервером и несколькими клиентами?
Может быть так, что у вас есть несколько клиентов, которые одновременно коммуницируют с вашим сервером. Пока каждая команда, отправляемая серверу, меньше PIPE_BUF (см. предыдущий вопрос), все эти клиенты могут использовать один и тот же именованный канал для отправки данных серверу. Все клиенты могут легко узнать имя входящего FIFO сервера.
Однако сервер не может использовать одиночный канал для обмена со всеми клиентами. Если больше одного клиента читают из одного и того же канала, то нет способа гарантировать, соответствующий клиент получит заданный ответ.
Решение состоит в том, что клиент должен создать собственный входящий канал перед отправкой данных на сервер, или сервер должен создавать исходящие каналы для клиентов после получения данных от клиента.
Использование идентификатора процесса клиента (process ID) в имени канала - обычный метод для идентификации клиента. При использовании каналов FIFO, именованных таким способом, каждый раз, когда клиент посылает команду серверу, он может включать в неё свой PID как часть команды. Любые возвращенные данные могут быть посланы через соответствующий именованный канал.
[Ссылки]
1. Unix Programming FAQ (v1.37) site:opennet.ru. 2. Sockets FAQ site:socket.dev. 3. FAQ программирования Linux: управление процессами. 4. Что такое PTY и TTY? 5. Опции GCC для поддержки отладки. |