14.
Открытие стандартного потока :
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int filedes, const char *type);
Получение дескриптора :
int fileno(FILE *fp);
Создание уникального временного файла
char *tmpnam(char *ptr);
Закрытие :
int fclose(FILE *fp);
freopen используется обычно для открытия стандартных ввода и вывода
Аргумент nype - это r , w , rw и т.д.
Мы можем читать или писать в следующих вариантах
1 По-символьно - чтение - getc , fgetc , запись - putc , fputc
2 По-строчно - чтение - fgets , запись - fputs
3 По-обьектно - чтение - fread , запись - fwrite
Программу копирования стандартного ввода на вывод можно реализовать с помощью первых 2-х
вариантов :
посимвольно :
int c;
while ((c = getc(stdin)) != EOF)
if (putc(c, stdout) == EOF)
err_sys("output error");
построчно :
char buf[MAXLINE];
while (fgets(buf, MAXLINE, stdin) != NULL)
if (fputs(buf, stdout) == EOF)
err_sys("output error");
Пример с обьектной бинарной записью :
struct {
short count;
long total;
char name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
err_sys("fwrite error");
2.
Функция select дает возможность управления массивом дескрипторов :
int select(int nfds,
fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict errorfds,
struct timeval *restrict timeout);
Первый аргумент nfds - это диапазон дескрипторов.
Второй параметр - дескрипторы на чтение , третий на запись .
Последний параметр форсирует возвращение из select по истечение времени, если он=NULL,
селект по идее подвисает .
При успешном возвращении функция возвращает число дескрипторов .
Макросы для работы с битами дескрипторов :
void FD_CLR(int fd, fd_set *fdset); - очищает указываемые биты
int FD_ISSET(int fd, fd_set *fdset); - проверяет биты
void FD_SET(int fd, fd_set *fdset); - устанавливает биты
void FD_ZERO(fd_set *fdset); - очищает все биты
Пример : Функция возвращает один из 2-х дескрипторов :
int whichisready(int fd1, int fd2)
{
int maxfd;
int nfds;
fd_set readset;
maxfd = (fd1 > fd2) ? fd1 : fd2;
FD_ZERO(&readset);
FD_SET(fd1, &readset);
FD_SET(fd2, &readset);
nfds = select(maxfd+1, &readset, NULL, NULL, NULL);
if (FD_ISSET(fd1, &readset)) return fd1;
if (FD_ISSET(fd2, &readset)) return fd2;
return -1;
}
Пример : чтение из нескольких файлов с помощью массива дескрипторов :
void monitorselect(int fd[], int numfds) {
char buf[BUFSIZE];
int bytesread;
int i;
int maxfd;
int numnow, numready;
fd_set readset;
maxfd = 0; /* установка диапазона дескрипторов */
for (i = 0; i < numfds; i++)
{
if (fd[i] >= maxfd)
maxfd = fd[i] + 1;
}
numnow = numfds;
while (numnow > 0)
{ /* continue monitoring until all are done */
FD_ZERO(&readset); /* set up the file descriptor mask */
for (i = 0; i < numfds; i++)
{
if (fd[i] >= 0)
FD_SET(fd[i], &readset);
}
numready = select(maxfd, &readset, NULL, NULL, NULL); /* which ready? */
for (i = 0; (i < numfds) && (numready > 0); i++)
{ /* read and process */
if (FD_ISSET(fd[i], &readset)) { /* this descriptor is ready */
bytesread = r_read(fd[i], buf, BUFSIZE);
numready--;
if (bytesread > 0)
docommand(buf, bytesread);
else { /* error occurred on this descriptor, close it */
r_close(fd[i]);
fd[i] = -1;
numnow--;
}
}
}
}
for (i = 0; i < numfds; i++)
if (fd[i] >= 0)
r_close(fd[i]);
}
3.
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
Это аналог select , возвращает число дескрипторов .
Select отличается от poll тем , что модифицирует флаги дескрипторов ,
поэтому при каждом вызове select в цикле надо сбрасывать эти флаги .
В poll нет maxfd .
Второй параметр - число дескрипторов .
Первый параметр - структура , в которой есть члены :
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
Аналогичный пример для poll :
void monitorpoll(int fd[], int numfds) {
char buf[BUFSIZE];
int bytesread;
int i;
int numnow = 0;
int numready;
struct pollfd *pollfd;
for (i=0; i< numfds; i++) /* initialize the polling structure */
if (fd[i] >= 0)
numnow++;
if ((pollfd = (void *)calloc(numfds, sizeof(struct pollfd))) == NULL)
return;
for (i = 0; i < numfds; i++) {
(pollfd + i)->fd = *(fd + i);
(pollfd + i)->events = POLLRDNORM;
}
while (numnow > 0) { /* Continue monitoring until descriptors done */
numready = poll(pollfd, numfds, -1);
if ((numready == -1) && (errno == EINTR))
continue; /* poll interrupted by a signal, try again */
else if (numready == -1) /* real poll error, can't continue */
break;
for (i = 0; i < numfds && numready > 0; i++) {
if ((pollfd + i)->revents) {
if ((pollfd + i)->revents & (POLLRDNORM | POLLIN) ) {
bytesread = r_read(fd[i], buf, BUFSIZE);
numready--;
if (bytesread > 0)
docommand(buf, bytesread);
else
bytesread = -1; /* end of file */
} else if ((pollfd + i)->revents & (POLLERR | POLLHUP))
bytesread = -1;
else /* descriptor not involved in this round */
bytesread = 0;
if (bytesread == -1) { /* error occurred, remove descriptor */
r_close(fd[i]);
(pollfd + i)->fd = -1;
numnow--;
}
}
}
}
for (i = 0; i < numfds; i++)
r_close(fd[i]);
free(pollf
}
4.
Стандартная библиотека I/O (fopen, fscanf, fprintf, fread, fwrite, fclose and so on)
использует файловые указатели.
Юниксовая I/O (open, read, write, close and ioctl) использует файловые дескрипторы.
Надо понимать , что первая лежит в отдельной библиотеке ,
а вторая зашита в ядро.
Организация дескрипторов в юниксе сделана на 3-х уровнях :
1 каждый процесс имеет свою таблицу дескрипторов
2 ядро имеет глобальную таблицу дескрипторов , в котором для каждого дескриптора
указываются процессы , которые работают с ним
3 имеется список нод в файловой системе
Поэтому , в обычных условиях , когде не делается блокировка ,
2 разных процесса , которые открывают один и тот же файл на чтение и на запись ,
могут получить разные результаты , поскольку ядро кеширует файлы .
Пример : записать строку в файл с помощью стандартной И/О :
FILE *myfp;
if ((myfp = fopen("/home/ann/my.dat", "w")) == NULL)
perror("Failed to open /home/ann/my.dat");
else
fprintf(myfp, "This is a test");
Приемер : каков порядок вывода ?
fprintf(stdout, "a");
fprintf(stderr, "a has been written\n");
fprintf(stdout, "b");
fprintf(stderr, "b has been written\n");
fprintf(stdout, "\n");
Сначала будет выведен stderr , потому что он не буферизуется , а потом stdout .
Пример : каков порядок вывода ?
fprintf(stdout, "a");
scanf("%d", &i);
fprintf(stderr, "a has been written\n");
fprintf(stdout, "b");
fprintf(stderr, "b has been written\n");
fprintf(stdout, "\n");
функция scanf пишет буфер на диск , поэтому сначала будет выведено "a"
Далее :
"a has been written\n"
"b has been written\n"
"b";
Пример : что напечатает программа ?
int main(void) {
printf("This is my output.");
fork();
return 0;
}
будет выведена 1 строка : This is my output.This is my output.
Поскольку стандартный вывод наследуется потомком , вывод сплюсуется в буффере сам с собой 2 раза ,
10.
Получение собственного id :
pthread_t pthread_self(void);
Сравнение тредов :
pthread_t pthread_equal(thread_t t1, pthread_t t2);
Создание треда :
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
Тут 4-й параметр - это аргумент,передаваемый в тредовую функцию , это может быть простой тип ,
массив , структура .
Пример : в тредовой функции читается массив из 2-х деструкторов , переданных в нее в качестве параметра :
void *copyfilemalloc(void *arg)
{ /* copy infd to outfd with return value */
int infd;
int outfd;
infd = *((int *)(arg));
outfd = *((int *)(arg) + 1);
r_close(infd);
r_close(outfd);
}
Сделать тред отсоединенным :
int pthread_detach(pthread_t thread);
Освобождение ресурсов присоединенного треда :
int pthread_join(pthread_t thread, void **value_ptr);
Тред может терминировать сам себя:
void pthread_exit(void *value_ptr);
Тред может терминировать другой тред , если у того PTHREAD_CANCEL_DISABLE :
int pthread_cancel(pthread_t thread);
Тред может изменить свой статус
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void);
В случае , когда тред возвращает какое-то значение , лучше всего для этого выделять дополнительную
память , куда помещать возвращаемое значение , и указатель на эту память , а еще лучше
поместить этот указатель в качестве 4-го параметра функции pthread_create.
Пример : дана тредовая функция
void *whichexit(void *arg) {
int n;
int np1[1];
int *np2;
char s1[10];
char s2[] = "I am done";
n = 3;
np1[0] = n;
np2 = (int *)malloc(sizeof(int *));
*np2 = n;
strcpy(s1, "Done");
return(NULL);
}
Что можно вернуть вместо NULL ?
1. n - нет
2. &n - нет
3. (int *)n - да
4. np1 - нет
5. np2 - да
6. s1 - нет
7. s2 - нет
8. "This works" - да
9. strerror(EINTR) - нет
Функции стандартной библиотеки С , которые могут вызываться из тредовых функций , разбиты на 2 категории :
1 trade-safe
2 non trade-safe
17.
Семафор - это целочисленная переменная , которая имеет 2 атомарных операции - wait(lock) и signal(unlock).
Если значение семафора больше нуля , wait уменьшает его на единицу .
Если значение семафора равно нулю , wait делает блокировку , а signal наоборот разблокирует.
Если нет заблокированных семафоров , signal увеличивает семафор на единицу .
Следующая условная схема показывает условную реализацию семафоров :
void wait(semaphore_t *sp)
{
if (sp->value > 0)
sp->value--;
else {
< добавляем процесс в sp->list>
< блокируем>
}
}
void signal(semaphore_t *sp)
{
if (sp->list != NULL)
< удвляем процесс из sp->list и помечаем его как готовый>
else
sp->value++;
}
Следующий псевдо-код защищает критическую секцию , если значение семафора = 1
wait(&S);
< critical section>
signal(&S);
< remainder section>
Семафор - это обьект типа sem_t. Семафоры бывают именованные и неименованные .
Инициализация семафора :
int sem_init(sem_t *sem, int pshared, unsigned value);
pshared устанавливает , может ли семафор использоваться другими процессами.
value обычно=1, указывая на то , что первый процесс может делать блокировку с помощью данного семафора
Пример : создание семафора , который может быть использован только тредами данного процесса :
sem_t semA;
sem_init(&semA, 0, 1)
sem_post - это реализация выше-описанной signal . Если нет тредов , заблокированных семафором ,
происходит инкремент семафора . Если тред заблокирован семафором , семафор обнуляется .
sem_wait - если семафор=0 , то тред блокируется до тех пор , пока не будет вызвана sem_post .
Пример : защита расшаренной переменной с помощью семафора
3 функции - initshared , getshared и incshared - соответственно инициализируют , получают и
увеличивают эту переменную . Переменная называется shared .
static int shared = 0;
static sem_t sharedsem;
int initshared(int val) {
if (sem_init(&sharedsem, 0, 1) == -1)
return -1;
shared = val;
return 0;
}
int getshared(int *sval) {
while (sem_wait(&sharedsem) == -1)
if (errno != EINTR)
return -1;
*sval = shared;
return sem_post(&sharedsem);
}
int incshared() {
while (sem_wait(&sharedsem) == -1)
if (errno != EINTR)
return -1;
shared++;
return sem_post(&sharedsem);
}
Пример : создается семафор , который передается в качестве параметра в массив из 10 тредов
sem_t semlock;
pthread_t *tids;
tids = (pthread_t *)calloc(10, sizeof(pthread_t));
sem_init(&semlock, 0, 1);
for (i = 0; i < 10; i++)
pthread_create(tids + i, NULL, thread_func, &semlock);
18.
Именованный семафор может быть использован различными процессами .
Имя его должно начинаться со слеша .
Установить коннект с семафором :
sem_t *sem_open(const char *name, int oflag, ...);
Пример : функция получения именованного семафора , либо его создания , если его нет :
#define PERMS (mode_t)(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FLAGS (O_CREAT | O_EXCL)
int getnamed(char *name, sem_t **sem, int val)
{
while (((*sem = sem_open(name, FLAGS, PERMS, val)) == SEM_FAILED) && (errno == EINTR)) ;
if (*sem != SEM_FAILED) return 0;
while (((*sem = sem_open(name, 0)) == SEM_FAILED) && (errno == EINTR)) ;
if (*sem != SEM_FAILED) return 0;
}
Пример : массив форкнутых дочерних процессов , который работает с критической секций ,
защищенной именованным семафором :
int getnamed(char *name, sem_t **sem, int val);
int main (int argc, char *argv[]) {
char buffer[BUFSIZE];
char *c;
pid_t childpid = 0;
int delay;
volatile int dummy = 0;
int i, n;
sem_t *semlockp;
for (i = 1; i < 10; i++)
if (childpid = fork()) break;
getnamed("/My_Semaphor", &semlockp, 1);
while (sem_wait(semlockp) == -1) /* entry section */
while (*c != '\0')
{ /* critical section */
fputc(*c, stderr);
c++;
for (i = 0; i < delay; i++)
dummy++;
}
if (sem_post(semlockp) == -1) {
/* exit section */
}
}
1. для получения списка переменных окружения нужно прочитать /proc/self/environ
и вывести его на экран . В этом файле строки разделены нулями , а не символами перевода строки.
#include < stdio.h>
#define BUF_SIZE 0x100
int main(int argc, char * argv[])
{
char buf[BUF_SIZE];
int len, i;
FILE * f;
f = fopen("/proc/self/environ", "r");
while((len = fread(buf, 1, BUF_SIZE-1, f)) > 0)
{
for (i = 0; i < len; i++) if (buf[i]==0) buf[i] = 10;
buf[len] = 0;
printf("%s", buf);
}
fclose(f);
return 0;
}
2.
#include < stdio.h>
#include < dirent.h>
int sel(struct dirent * d)
{ return 1; // всегда подтверждаем
}
int main(int argc, char ** argv)
{
int i, n;
struct dirent ** entry;
if (argc != 2)
{
printf("Использование: %s < директория>\n", argv[0]);
return 0;
}
n = scandir(argv[1], &entry, sel, alphasort);
if (n < 0)
{
printf("Ошибка чтения директории\n");
return 1;
}
for (i = 0; i < n; i++)
printf("%s inode=%i\n", entry[i]->d_name, entry[i]->d_ino);
return 0;
}
Функция scandir() создает список элементов указанной директории. Ей
необходимо передать указатель на функцию обратного вызова, которая, получая
данные об очередном элементе, принимает решение, включать этот элемент в
результирующий список. В нашем примере это функция sel(). Если при очередном
вызове функция sel() вернет значение 0, соответствующий элемент директории не
будет включен в конечный список. Последний параметр scandir - функция
сортировки элементов директории. Мы используем функцию alphasort(),
сортирующую элементы в лексикографическом порядке.
Данные об элементах директории передаются в структурах dirent.
Кроме имени файла dirent содержит номер inode для этого элемента
(простым программам обычно не зачем знать номера
inode, но, чтобы наш пример чем-то отличался от стандартного, мы включаем эту
информацию).
4. это такой файл , фактический размер которого больше реального
#include < stdio.h>
#include < string.h>
#define BIG_SIZE 0x1000000
int main(int argc, char * argv[])
{
FILE *f;
f = fopen(argv[1], "w");
if (f == NULL)
{
printf("Невозможно создать файл: %s", argv[1]);
return 1;
}
fwrite(argv[1], 1, strlen(argv[1]), f);
fseek(f, BIG_SIZE, SEEK_CUR);
fwrite(argv[1], 1, strlen(argv[1]), f);
fclose(f);
}
Если скомпилировать эту программу под именем makehole и запустить
makehole bighole.txt
то на диске будет создан файл bighole.txt. Команда ls –al сообщит нам, что
размер файла составляет чуть больше 16 мегабайт (см. значение константы
BIG_SIZE в программе). Однако, с помощью команды
du bighole.txt
мы узнаем, что на диске этот файл занимает 24 байта. Причиной появления
пропусков в открытом для записи файле стало смещение с помощью функции
fseek() в область после конца файла. Выход за пределы файла с помощью fseek() –
стандартный метод получения разреженных файлов. В момент вызова fseek() в
нашей программе позиция записи находится в конце файла. Флаг SEEK_CUR
указывает, что смещение отсчитывается от текущей позиции. Таким образом, в
файле образуется пропуск, величина которого в байтах соответствует значению
BIG_SIZE. При чтении пустых блоков в разреженном файле функция чтения
данных будет возвращать блоки, заполненные нулями.
5.
Блокировка областей файла позволяет нескольким программам совместно
работать с содержимым одного и того же файла, не мешая друг другу, или,
точнее, мешая друг другу испортить данные. Мы рассмотрим интерфейс
блокировки областей, основанный на использовании функции fcntl(2).
Нужно собрать и запустить несколько экземпляров следующей программы.
Первый экземпляр testlocks создаст файл testlocks.txt.
Каждый процесс заблокирует 64 байта в этом файле и сделает запись в
заблокированную область. Второй, третий и все последующие экземпляры
процессов сообщат, какие области файла уже заблокированы другими процессами.
Завершить программу testlocks можно, нажав любую символьную клавишу и,
затем, ввод.
#include < stdio.h>
#include < unistd.h>
#include < fcntl.h>
#include < string.h>
int main(int argc, char * argv[])
{
int fd;
char str[64];
memset(str, 32, 64);
struct flock fi;
int off;
sprintf(str, "Запись сделана процессом %i", getpid());
fd = open("testlocks.txt", O_RDWR|O_CREAT);
fi.l_type = F_WRLCK;
fi.l_whence = SEEK_SET;
fi.l_start = 0;
fi.l_len = 64;
off = 0;
while (fcntl(fd, F_SETLK, &fi) == -1)
{
fcntl(fd, F_GETLK, &fi);
printf("байты %i - %i заблокированы процессом %i\n", off, off+64, fi.l_pid);
off += 64;
fi.l_start = off;
}
lseek(fd, off, SEEK_SET);
write(fd, str, strlen(str));
getchar();
fi.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &fi) == -1)
printf("Ошибка разблокирования\n");
close(fd);
}
В структуру flock записывается информация о блокировке следующего типа
Поле Значение
l_type Тип блокировки: записи – F_RDLCK, чтения – F_WRLCK, сброс – F_UNLCK.
l_whence Точка отсчета смещения
l_start Начальный байт области
l_len Длина области
l_pid Идентификатор процесса, установившего
6.
Чаще всего внутри-программные каналы использовуются тогда, когда
программа запускает другую программу и считывает данные, которые та выводит
в свой стандартный поток вывода. С помощью этого трюка разработчик может
использовать в своей программе функциональность другой программы, не
вмешиваясь во внутренние детали ее работы. Для решения этой задачи мы
воспользуемся функциями popen(3) и pclose(3). Формально эти функции подобны
функциям fopen(3) и fclose(3). Функция popen() запускает внешнюю программу и
возвращает вызвавшему ее приложению указатель на структуру FILE, связанную
либо со стандартным потоком ввода, либо со стандартным потоком вывода
запущенного процесса. Первый параметр функции popen() - строка, содержащая
команду, запускающую внешнюю программу. Второй параметр определяет, какой
из стандартных потоков (вывода или ввода) будет возвращен. Аргумент “w”
соответствует потоку ввода запускаемой программы, в этом случае приложение,
вызвавшее popen(), записывает данные в поток. Аргумент “r” соответствует потоку
вывода. Функция pclose() завершает работу с внешним приложением и закрывает
канал.
Программа запускает шелл, и записывает данные, выводимые командой в шелле,
одновременно на стандартный терминал и в файл log.txt (аналогичными
функциями обладает стандартная команда tee).
Например, если скомпилировать программу:
gcc makelog.c -o makelog
а затем скомандовать
makelog "ls -al"
на экране терминала будут распечатаны данные, выводимые командой
оболочки ls -al, а в рабочей директории программы makelog будет создан файл
log.txt, содержащий те же данные.
Рассмотрим строку программы
f = popen(argv[1], "r");
argv[1] - это не имя файла , а команда "ls -al"
Если вызов popen() был успешен, мы можем считывать данные,
выводимые запущенной командой, с помощью обычной функции fread(3):
fread(buf, 1, BUF_SIZE, f)
Для вывода данных, прочитанных с
помощью fread(), на терминал мы используем функцию write() с указанием
дескриптора стандартного потока вывода:
write(1, buf, len);
Текст программы :
/*
Popen/Pclose Demo by Andrei Borovsky .
This code is freeware.
*/
#include < stdio.h>
#include < errno.h>
#define BUF_SIZE 0x100
int main(int argc, char * argv[])
{
FILE * f;
FILE * o;
int len;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("использование: makelog \"\"\n");
return -1;
}
f = popen(argv[1], "r");
if (f == NULL)
{
perror("ошибка:\n");
return -1;
}
o = fopen("log.txt", "w");
while ((len = fread(buf, 1, BUF_SIZE, f)) != 0)
{
write(1, buf, len);
fwrite(buf, 1, len, o);
}
pclose(f);
fclose(o);
return 0;
}
7.
Канал , создаваемый функцией pipe(2), на уровне интерфейса представляется двумя
дескрипторами файлов, один из которых служит для чтения данных, а другой - для
записи. Каналы не поддерживают произвольный доступ, т. е. данные могут
считываться только в том же порядке, в котором они записывались.
Неименованные каналы используются преимущественно вместе с функцией fork(2)
и служат для обмена данными между родительским и дочерним процессами. Для
организации подобного обмена данными, сначала, с помощью функции pipe(),
создается канал. Функции pipe() передается единственный параметр - массив
типа int, состоящий из двух элементов. В первом элементе массива функция
возвращает дескриптор файла, служащий для чтения данных из канала (выход
канала), а во втором - дескриптор для записи (вход). Затем, с помощью функции
fork() процесс «раздваивается». Дочерний процесс наследует от родительского
процесса оба дескриптора, открытых с помощью pipe(), но, также как и
родительский процесс, он должен использовать только один из дескрипторов.
Направление передачи данных между родительским и дочерним процессом
определяется тем, какой дескриптор будет использоваться родительским
процессом, а какой - дочерним. Продемонстрируем изложенное на простом
примере программы pipes.c, использующей функции pipe() и fork().
#include < stdio.h>
#include < string.h>
#include < sys/types.h>
int main (int argc, char * argv[])
{
int pipedes[2];
pid_t pid;
pipe(pipedes);
pid = fork();
if ( pid > 0 )
{
char *str = "String passed via pipe\n";
close(pipedes[0]);
write(pipedes[1], (void *) str, strlen(str) + 1);
close(pipedes[1]);
}
else
{
char buf[1024];
int len;
close(pipedes[1]);
while ((len = read(pipedes[0], buf, 1024)) != 0)
write(2, buf, len);
close(pipedes[0]);
}
return 0;
}
Оба дескриптора канала хранятся в переменной pipedes. После вызова fork()
процесс раздваивается и родительский процесс (тот, в котором fork() вернула
ненулевое значение, равное PID дочернего процесса) закрывает дескриптор,
открытый для чтения, и записывает данные в канал, используя дескриптор,
открытый для записи (pipedes[1]). Дочерний процесс (в котором fork() вернула 0)
закрывает дескриптор, открытый для записи, и затем считывает данные из канала,
используя дескриптор, открытый для чтения (pipedes[0]). Назначение
дескрипторов легко запомнить, сопоставив их с аббревиатурой I/O (первый
дескриптор - для чтения (input), второй - для записи (output)).
Для передачи данных с помощью каналов используются специальные области
памяти (созданные ядром системы), называемые буферами каналов (pipe buffers).
Одна из важных особенностей буферов каналов заключается в том, что даже если
предыдущая запись заполнила буфер не полностью, повторная запись данных в
буфер становится возможной только после того, как прежде записанные данные
будут прочитаны. Это означает, что если разные процессы, пишущие данные в
один и тот же канал, передают данные блоками, размеры которых не превышают
объем буферов, данные из блоков, записанных разными процессами, не будут
перемешиваться между собой. Использование этой особенности каналов
существенно упрощает синхронизацию передачи данных.
8.
Напишем небольшую программу, которая запускает
утилиту netstat, читает данные, выводимые этой утилитой, и распечатывает их на
экране. Если бы мы использовали для этой цели функцию popen(), то получили бы
доступ к потоку вывода netstat с помощью вызова
popen("netstat", "r");
Этот способ прост, но не эффективен. Мы напишем другую программу (файл
printns.c). Структура этой программы та же, что и в предыдущем примере, только
теперь родительский процесс читает данные с помощью канала явным образом.
Самое интересное происходит в дочернем процессе, где выполняется
последовательность функций:
close(pipedes[0]);
dup2(pipedes[1], 1);
execve("/bin/netstat", NULL, NULL);
С помощью функции dup2(2) мы перенаправляем стандартный поток вывода
дочернего процесса (дескриптор стандартного потока вывода равен 1) в канал,
используя дескриптор pipdes[1], открытый для записи. Далее с помощью функции
execve(2) мы заменяем образ дочернего процесса процессом netstat (обратите
внимание, что поскольку в нашем распоряжении нет оболочки с ее переменной
окружения PATH, путь к исполнимому файлу netstat нужно указывать полностью).
В результате родительский процесс может читать стандартный вывод netstat через
поток, связанный с дескриптором pipdes[0] (и никакой оболочки!).
текст программы :
#include < stdio.h>
#include < string.h>
#include < sys/types.h>
#include < unistd.h>
#define BUF_SIZE 0x100
int main (int argc, char * argv[])
{
int pipedes[2];
pid_t pid;
pipe(pipedes);
pid = fork();
if ( pid > 0 )
{
char buf[BUF_SIZE];
int len;
close(pipedes[1]);
while ((len = read(pipedes[0], buf, BUF_SIZE)) > 0)
write(1, buf, len);
close(pipedes[0]);
}
else
{
close(pipedes[0]);
dup2(pipedes[1], 1);
execve("/bin/netstat", NULL, NULL);
}
return 0;
}
9.
Для передачи данных между неродственными процессами мы воспользуемся
механизмом именованных каналов (named pipes), который позволяет каждому
процессу получить свой, «законный» дескриптор канала. Передача данных в этих
каналах (как, впрочем, и в однонаправленных неименованных каналах)
подчиняется принципу FIFO (первым записано - первым прочитано), поэтому в
англоязычной литературе иногда можно встретить названия FIFO pipes или
просто FIFOs. Именованные каналы отличаются от неименованных наличием
имени (странно, не правда ли?), то есть идентификатора канала, потенциально
видимого всем процессам системы. Для идентификации именованного канала
создается файл специального типа pipe. Это еще один представитель семейства
виртуальных файлов Unix, не предназначенных для хранения данных (размер
файла канала всегда равен нулю). Файлы именованных каналов являются
элементами VFS, как и обычные файлы Linux, и для них действуют те же правила
контроля доступа. Для создания файлов именованных каналов можно
воспользоваться функцией mkfifo(3). Первый параметр этой функции - строка, в
которой передается имя файла канала, второй параметр - маска прав доступа к
файлу. Функции mkfifo() создает канал и файл соответствующего типа. Если
указанный файл канала уже существует, mkfifo() возвращает -1, (переменная errno
принимает значение EEXIST). После создания файла канала, процессы,
участвующие в обмене данными, должны открыть этот файл либо для записи, либо
для чтения. Обратите внимание на то, что после закрытия файла канала файл (и
соответствующий ему канал) продолжают существовать. Для того чтобы закрыть
сам канал, нужно удалить его файл, например с помощью последовательных
вызовов unlink(2).
Рассмотрим работу именованного канала на примере простой системы клиент-
сервер. Программа-сервер создает канал и передает в него текст, вводимый
пользователем с клавиатуры. Программа-клиент читает текст и выводит его на
терминал. Программы из этого примера можно рассматривать как упрощенный
вариант системы мгновенного обмена сообщениями между пользователями
многопользовательской ОС. Исходный текст программы-сервера хранится в файле
typeserver.c. Вызов функции mkfifo() создает файл-идентификатор канала в
рабочей директории программы:
mkfifo(FIFO_NAME, 0600);
где FIFO_NAME - макрос, задающий имя файла канала (в нашем случае -
"./fifofile").
В качестве маски доступа мы используем восьмеричное значение 0600,
разрешающее процессу с аналогичными реквизитами пользователя чтение и
запись (можно было бы использовать маску 0666, но на мы на всякий случай
воздержимся от упоминания Числа Зверя, пусть даже восьмеричного, в нашей
программе). Для краткости мы не проверяем значение, возвращенное mkfifo(), на
предмет ошибок. В результате вызова mkfifo() с заданными параметрами в рабочей
директории программы должен появиться специальный файл fifofile. Файл-
менеджер KDE отображает файлы канала с помощью красивой пиктограммы,
изображающей приоткрытый водопроводный кран. Далее в программе-сервере мы
просто открываем созданный файл для записи:
f = fopen(FIFO_NAME, "w");
Считывание данных, вводимых пользователем, выполняется с помощью
getchar(), а с помощью функции fputc() данные передаются в канал. Работа
сервера завершается, когда пользователь вводит символ “q”. Исходный текст
программы-клиента можно найти в файле typeclient.c. Клиент открывает файл
fifofile для чтения как обычный файл:
f = fopen(FIFO_NAME, "r");
Символы, передаваемые по каналу, считываются с помощью функции fgetc() и
выводятся на экран терминала с помощью putchar(). Каждый раз, когда
пользователь сервера наживает ввод, функция fflush(), вызываемая сервером (см.
файл typeserver.c), выполняет принудительную очистку буферов канала, в
результате чего клиент считывает все переданные символы. Получение символа
“q” завершает работу клиента.
текст программы-клиента:
#include < stdio.h>
#define FIFO_NAME "./fifofile"
int main ()
{
FILE * f;
char ch;
f = fopen(FIFO_NAME, "r");
do
{
ch = fgetc(f);
putchar(ch);
} while (ch != 'q');
fclose(f);
unlink(FIFO_NAME);
return 0;
}
текст программы-сервера :
#include < stdio.h>
#include < sys/types.h>
#include < sys/stat.h>
#define FIFO_NAME "./fifofile"
int main(int argc, char * argv[])
{
FILE * f;
char ch;
mkfifo(FIFO_NAME, 0600);
f = fopen(FIFO_NAME, "w");
if (f == NULL)
{
printf("Не удалось открыть файл\n");
return -1;
}
do
{
ch = getchar();
fputc(ch, f);
if (ch == 10) fflush(f);
} while (ch != 'q');
fclose(f);
unlink(FIFO_NAME);
return 0;
}
Запустите сначала сервер, потом клиент в разных окнах терминала. Печатайте
текст в окне сервера. После каждого нажатия клавиши [Enter] клиент должен
отображать строку, напечатанную на сервере.
11.
В качестве примера мы рассмотрим комплекс из двух приложений, клиента и
сервера, использующих сетевые сокеты для обмена данными. Текст программы
сервера вы найдете в файле netserver.c, ниже мы приводим некоторые фрагменты.
Прежде всего, мы должны получить дескриптор сокета:
sock = socket(AF_INET, SOCK_STREAM, 0);
if (socket < 0) {
printf("socket() failed: %d\n", errno);
return EXIT_FAILURE;
}
В первом параметре функции socket() мы передаем константу AF_INET,
указывающую на то, что открываемый сокет должен быть сетевым. Значение
второго параметра требует, чтобы сокет был потоковым. Далее мы, как и в случае
сокета в файловом пространстве имен, вызываем функцию bind():
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(port);
if (bind(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
printf("bind() failed: %d\n", errno);
return EXIT_FAILURE;
}
Переменная serv_addr, - это структура типа sockaddr_in. Тип sockaddr_in
специально предназначен для хранения адресов в формате Интернета. Самое
главное отличие sockaddr_in от sockaddr_un – наличие параметра sin_port,
предназначенного для хранения значения порта. Функция htons() переписывает
двухбайтовое значение порта так, чтобы порядок байтов соответствовал
принятому в Интернете (см. врезку). В качестве семейства адресов мы указываем
AF_INET (семейство адресов Интернета), а в качестве самого адреса –
специальную константу INADDR_ANY. Благодаря этой константе наша программа
сервер зарегистрируется на всех адресах той машины, на которой она
выполняется.
Мы вызываем функцию listen(2), которая переводит сервер в режим ожидания запроса на
соединение:
listen(sock, 1);
Второй параметр listen() – максимальное число соединений, которые сервер
может обрабатывать одновременно. Далее мы вызываем функцию accept(2),
которая устанавливает соединение в ответ на запрос клиента:
newsock = accept(sock, (struct sockaddr *) &cli_addr, &clen);
if (newsock < 0) {
printf("accept() failed: %d\n", errno);
return EXIT_FAILURE;
}
Получив запрос на соединение, функция accept() возвращает новый сокет,
открытый для обмена данными с клиентом, запросившим соединение. Сервер как
бы перенаправляет запрошенное соединение на другой сокет, оставляя сокет sock
свободным для прослушивания запросов на установку соединения. Второй
параметр функции accept() содержит сведения об адресе клиента, запросившего
соединение, а третий параметр указывает размер второго. Так же как и при
вызове функции recvfom(), мы можем передать NULL в последнем и
предпоследнем параметрах. Для чтения и записи данных сервер использует
функции read() и write(), а для закрытия сокетов, естественно, close().
В программе-клиенте (netclient.c) нам, прежде всего, нужно решить задачу, с
которой мы не сталкивались при написании сервера, а именно выполнить
преобразование доменного имени сервера в его сетевой адрес. Разрешение
доменных имен выполняет функция gethostbyname():
server = gethostbyname(argv[1]);
if (server == NULL) {
printf("Host not found\n");
return EXIT_FAILURE;
}
Функция получает указатель на строку с Интернет-именем сервера (например,
www.unix.com или 192.168.1.16) и возвращает указатель на структуру hostent
(переменная server), которая содержит имя сервера в приемлемом для
дальнейшего использования виде. При этом, если необходимо, выполняется
разрешение доменного имени в сетевой адрес. Далее мы заполняем поля
переменной serv_addr (структуры sockaddr_in) значениями адреса и порта:
serv_addr.sin_family = AF_INET;
memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length);
serv_addr.sin_port = htons(port);
Программа-клиент открывает новый сокет с помощью вызова функции socket()
аналогично тому, как это делает сервер (дескриптор сокета, который возвращает
socket() мы сохраним в переменной sock), и вызывает функцию connect(2) для
установки соединения:
if (connect(sock, &serv_addr, sizeof(serv_addr)) < 0) {
printf("connect() failed: %d", errno);
return EXIT_FAILURE;
}
Теперь сокет готов к передаче и приему данных. Программа-клиент считывает
символы, вводимые пользователем в окне терминала. Когда пользователь
нажимает <Ввод> программа передает данные серверу, ждет ответного
сообщения сервера и распечатывает его.
14.
#include < stdio.h>
#include < stdlib.h>
#include < signal.h>
void term_handler(int i)
{ printf ("Terminating\n");
exit(EXIT_SUCCESS);
}
int main(int argc, char ** argv) {
struct sigaction sa;
sigset_t newset;
sigemptyset(&newset);
sigaddset(&newset, SIGHUP);
sigprocmask(SIG_BLOCK, &newset, 0);
sa.sa_handler = term_handler;
sigaction(SIGTERM, &sa, 0);
printf("My pid is %i\n", getpid());
printf("Waiting...\n");
while(1) sleep(1);
return EXIT_FAILURE;
}
Наша программа делает две вещи: обрабатывает сигнал SIGTERM (при
получении этого сигнала программа выводит диагностическое сообщение и
завершает свою работу) и блокирует сигнал SIGHUP, так что этот сигнал не может
завершить ее работу. В тексте программы мы первым делом определяем функцию-
обработчик сигнала SIGTERM term_handler(). Функции-обработчики сигналов – это
обычные функции Си, они имеют доступ ко всем глобально видимым переменным
и функциям. Однако, поскольку мы не знаем, в какой момент выполнения
программы будет вызвана функция-обработчик, мы должны проявлять особую
осторожность при обращении к глобальным структурам данных из этой функции.
Для функций, обрабатывающих потоки, существует и еще одно важное требование
– реентерабильность. Поскольку обработчик сигнала может быть вызван в любой
точке выполнения программы (а при не кототорых условиях во время обработки
одного сигнала может быть вызван другой обработчик сигнала) в обработчиках
додлжны использоваться функции, которые удовлетворяют требованию
реентерабельности, то есть, могут быть вызваны в то время, когда они уже
вызваны где-то в другой точке программы. Фактически, требование
реентерабельности сводится к тому, чтобы функция не использовала никаких
глобальных ресурсов, не позаботившись о синхронизации доступа к этим
ресурсам. Некоторые функции ввода-вывода, в том числе, функция printf(),
которую мы (и не только мы) используем в примерах обработчиков сигналов,
реентерабельными не являются. Это значит, что выводу одной функции printf()
может помешать вывод другой функции. В приложении приводится список
реентерабельных функций, которые безопасно вызвать из обработчиков сигналов.
Единственным параметром нашего варианта функции-обработчика сигнала (в
Unix-системах существует и другой вариант) является переменная типа int, в
которой передается номер сигнала, вызвавшего обработчик. Нам этот номер не
нужен, поскольку мы знаем, что только один сигнал, - SIGTERM, может вызвать
нашу функцию, однако, в принципе, ничто не мешает нам использовать одну
функцию для обработки нескольких разных сигналов, и тогда параметр функции-
обработчика будет иметь для нас смысл. Функция-обработчик не возвращает
никакого значения, что вполне логично, так как она вызывается не нашей
программой, а неким системным компонентом. Особый интерес представляет
завершение программы из обработчика сигнала. Назначение обработчика сигналу
SIGTERM означает, что умалчиваемое действие сигнала, – завершение
программы, не будет выполняться автоматически, и нам необходимо (если,
конечно, мы хотим, чтобы этот сигнал завершал программу) позаботиться об этом
явным образом. Если вы закомментируете вызов exit() в нашем примере, то
увидите, что программа не будет завершать по получении сигнала SIGTERM. В
принципе, вы можете придать сигналу SIGTERM совершенно иной смысл,
например, оповещать программу о наступлении времени вашей любимой
телепередачи (или о выходе нового номера журнала Linux Format), однако
назначать стандартным сигналам нестандартные действия категорически не
рекомендуется. Обработчик SIGTERM предназначен для того, чтобы, по
требованию системы или пользователя, программа могла быстро и элегантно
закончить текущую задачу и завершить свое выполнение. Именно этим
обработчик и должен заниматься.
Перейдем теперь к тексту главной функции программы. Установка и удаление
обработчиков сигналов осуществляются функцией sigaction(2). Первым
параметром этой функции является номер сигнала, а в качестве второго и
третьего параметров следует передать указатели на структуру sigaction. Эта
структура содержит данные об операции, выполняемой над обработчиком сигнала.
Второй параметр sigaction() служит для передачи новых значений для обработки
сигнала, а третий – возвращает ранее установленные значения. В таблице 1
приводится краткое описание полей структуры sigaction.
Таблица 1. Поля структуры sigaction.
Поле Значение
sa_handler Указатель на функцию обработчик сигнала или константа.
sa_mask Маска сигналов, блокируемых на время вызова обработчика.
sa_flags Дополнительные флаги.
Поле sa_handler должно содержать либо адрес функции-обработчика, либо
специальную константу, указывающую, что нужно делать с сигналом. Константа
SIG_IGN указывает, что сигнал следует игнорировать, а константа SIG_DFL – что
нужно восстановить обработку сигнала, заданную системой по умолчанию. Поле
sa_mask позволяет заблокировать некоторое множество сигналов на время
выполнения обработчика данного сигнала. Делается это для того, чтобы
обработка других сигналов не могла прервать обработку данного (это может быть
необходимо, особенно, если один обработчик обрабатывает несколько разных
сигналов). Параметр sa_flags позволяет задать ряд флагов для выполнения более
тонкой настройки обработчика сигналов. Например, флаг SA_RESETHAND
указывает, что после завершения обработки сигнала заданным обработчиком
должен быть восстановлен обработчик, заданный по умолчанию, так что все
последующие сигналы будут обрабатываться умалчиваемым обработчиком.
В результате вызова функции sigaction() мы устанавливаем обработчик сигнала
SIGTERM. Затем наша программа распечатывает значение PID (это значение
понадобится нам для вызова команды kill) и входит в бесконечный цикл, из
которого она может быть выведена одним из сигналов. Следует отметить, что
функция sleep() возвращает управление (возобновляет выполнение программы
раньше срока) если обработчик какого-либо сигнала возвращает управление в этот
момент. Иначе говоря, любой обрабатываемый сигнал прерывает выполнение
sleep(). Впрочем, в нашем примере с бесконечным циклом это не помогло бы
программе завершиться. Сигнал SIGTERM приведет к тому, что программа выдаст
диагностическое сообщение и завершит работу, а сигналы SIGINT и SIGABRT – к
тому, что программа завершится без всякого сообщения. Скомпилируйте и
запустите программу в окне терминала. В другом окне скомандуйте
kill
где PID – идентификатор процесса программы. Вы увидите, что перед тем как
завершиться программа выдает диагностическое сообщение, тогда как при
завершении с помощью Ctrl-C никакого сообщения не выводится.
17.
Если вы хотите, чтобы новая программа работала одновременно со старой, нужно сначала
раздвоить процесс с помощью 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, достаточно указывать только имя исполнимого файла. Второй параметр
функции execvp() соответствует второму параметру execve(). У функции execvp()
нет параметра для передачи набора переменных среды окружения, но это не
значит, что запущенная программа не получит эти переменные. Программе будет
передана копия набора переменных окружения родительского процесса.
18.
Вы можете изменить переменные среды окружения не только путем
модификации списка 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()
для удаления всего списка переменных. Под удалением переменных окружения
подразумевается их удаление из среды окружения текущего процесса и его
потомков. Это удаление никак не повлияет на переменные среды окружения
других процессов.
19.
Рассмотрим использование функции 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"
24.
Ниже приводится фрагмент листинга программы threads
#include < stdlib.h>
#include < stdio.h>
#include < errno.h>
#include < pthread.h>
void * thread_func(void *arg)
{ int i;
int loc_id = * (int *) arg;
for (i = 0; i < 4; i++) {
printf("Thread %i is running\n", loc_id);
sleep(1);
}
}
int main(int argc, char * argv[])
{ int id1, id2, result;
pthread_t thread1, thread2;
id1 = 1;
result = pthread_create(&thread1, NULL, thread_func, &id1);
if (result != 0) {
perror("Creating the first thread");
return EXIT_FAILURE;
}
id2 = 2;
result = pthread_create(&thread2, NULL, thread_func, &id2);
if (result != 0) {
perror("Creating the second thread");
return EXIT_FAILURE;
}
result = pthread_join(thread1, NULL);
if (result != 0) {
perror("Joining the first thread");
return EXIT_FAILURE;
}
result = pthread_join(thread2, NULL);
if (result != 0) {
perror("Joining the second thread");
return EXIT_FAILURE;
}
printf("Done\n");
return EXIT_SUCCESS;
}
Рассмотрим сначала функцию thread_func(). Это и
есть функция потока. Наша функция потока очень проста. В качестве аргумента
ей передается указатель на переменную типа int, в которой содержится номер
потока. Функция потока распечатывает этот номер несколько раз с интервалом в
одну секунду и завершает свою работу. В функции main() вы видите две
переменных типа pthread_t. Мы собираемся создать два потока и у каждого из них
должен быть свой идентификатор. Вы также видите две переменные типа int, id1
и id2, которые используются для передачи функциям потоков их номеров. Сами
потоки создаются с помощью функции pthread_create().В этом примере мы не
модифицируем атрибуты потоков, поэтому во втором параметре в обоих случаях
передаем NULL. Вызывая pthread_create() дважды, мы оба раза передаем в
качестве третьего параметра адрес функции thread_func, в результате чего два
созданных потока будут выполнять одну и ту же функцию. Функция, вызываемая
из нескольких потоков одновременно, должна обладать свойством
реентерабельности (этим же свойством должны обладать функции, допускающие
рекурсию). Реентерабельная функция, это функция, которая может быть вызвана
повторно, в то время, когда она уже вызвана (отсюда и происходит ее название).
Реентерабельные функции используют локальные переменные (и локально
выделенную память) в тех случаях, когда их не-реентерабельные аналоги могут
воспользоваться глобальными переменными.
Мы вызываем последовательно две функции pthread_join() для того, чтобы
дождаться завершения обоих потоков. Если мы хотим дождаться завершения всех
потоков, порядок вызова функций pthread_join() для разных потоков, очевидно, не
имеет значения.
Для того, чтобы скомпилировать программу threads.c, необходимо дать
команду:
gcc threads.c -D_REENTERANT -I/usr/include/nptl -L/usr/lib/nptl –lpthread
-o threads
Команда компиляции включает макрос _REENTERANT. Этот макрос указывает,
что вместо обычных функций стандартной библиотеки к программе должны быть
подключены их реентерабельные аналоги. Реентерабельный вариант библиотеки
glibc написан таким образом, что вы, скорее всего, вообще не обнаружите никаких
различий в работе с реентерабельными функциями по сравнению с их обычными
аналогами. Мы указываем компилятору путь для поиска заголовочных файлов и
путь для поиска библиотек /usr/include/nptl и /usr/lib/nptl соответственно. Наконец,
мы указываем компоновщику, что программа должна быть связана с библиотекой
libpthread, которая содержит все специальные функции, необходимые для работы
с потоками.
25.
Функции потоков можно рассматривать как вспомогательные программы,
находящиеся под управлением функции main(). Точно так же, как при управлении
процессами, иногда возникает необходимость досрочно завершить процесс,
многопоточной программе может понадобиться досрочно завершить один из
потоков. Для досрочного завершения потока можно воспользоваться функцией
pthread_cancel(3). Единственным аргументом этой функции является
идентификатор потока. Функция pthread_cancel() возвращает 0 в случае успеха и
ненулевое значение в случае ошибки. Несмотря на то, что pthread_cancel() может
завершить поток досрочно, ее нельзя назвать средством принудительного
завершения потоков. Дело в том, что поток может не только самостоятельно
выбрать порядок завершения в ответ на вызов pthread_cancel(), но и вовсе
игнорировать этот вызов. Вызов функции pthread_cancel() следует рассматривать
как запрос на выполнение досрочного завершения потока. Функция
pthread_setcancelstate(3) определяет, будет ли вызвавший ее поток реагировать на
обращение к нему с помощью pthread_cancel(), или не будет. У функции
pthread_setcancelstate() два параметра, параметр state типа int и параметр
oldstate типа «указатель на int». В первом параметре передается новое значение,
указывающее, как поток должен реагировать на запрос pthread_cancel(), а в
переменную, чей адрес был передан во втором параметре, функция записывает
прежнее значение. Если прежнее значение вас не интересует, во втором
параметре можно передать NULL. Чаще всего функция pthread_setcancelstate()
используется для временного запрета завершения потока. Допустим, мы
программируем поток, и знаем, что при определенных условиях программа может
потребовать его досрочного завершения. Но в нашем потоке есть участок кода, во
время выполнения которого завершать поток крайне нежелательно. Мы можем
оградить этот участок кода от досрочного завершения с помощью пары вызовов
pthread_setcancelstate():
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
... //Здесь поток завершать нельзя
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
Первый вызов pthread_setcancelstate() запрещает досрочное завершение
потока, второй – разрешает. Если запрос на досрочное завершение потока
поступит в тот момент, когда поток игнорирует эти запросы, выполнение запроса
будет отложено до тех пор, пока функция pthread_setcancelstate() не будет вызвана
с аргументом PTHREAD_CANCEL_ENABLE.
26.
Рассмотрим программу
#include < stdlib.h>
#include < stdio.h>
#include < errno.h>
#include < pthread.h>
#include < semaphore.h>
sem_t sem;
void * thread_func(void *arg)
{ int i;
int loc_id = * (int *) arg;
sem_post(&sem);
for (i = 0; i < 4; i++) {
printf("Thread %i is running\n", loc_id);
sleep(1);
}
}
int main(int argc, char * argv[])
{ int id, result;
pthread_t thread1, thread2;
id = 1;
sem_init(&sem, 0, 0);
result = pthread_create(&thread1, NULL, thread_func, &id);
if (result != 0) {
perror("Creating the first thread");
return EXIT_FAILURE;
}
sem_wait(&sem);
id = 2;
result = pthread_create(&thread2, NULL, thread_func, &id);
if (result != 0) {
perror("Creating the second thread");
return EXIT_FAILURE;
}
result = pthread_join(thread1, NULL);
if (result != 0) {
perror("Joining the first thread");
return EXIT_FAILURE;
}
result = pthread_join(thread2, NULL);
if (result != 0) {
perror("Joining the second thread");
return EXIT_FAILURE;
}
sem_destroy(&sem);
printf("Done\n");
return EXIT_SUCCESS;
}
В новом варианте программы мы используем одну переменную id для передачи
значения обоим потокам. Если вы скомпилируете и запустите программу threads2,
то увидите, что она работает корректно. Секрет нашего успеха заключается в
использовании средств синхронизации. Для синхронизации потоков мы
задействовали семафоры. Читатели этой серии статей уже знакомы с семафорами
System V, предназначенными для синхронизации процессов. В данном случае мы
применяем семафоры другого типа – семафоры POSIX, которые специально
предназначены для работы с потоками. Все объявления функций и типов,
относящиеся к этим семафорам, можно найти в файле
/usr/include/nptl/semaphore.h. Семафоры POSIX создаются (инициализируются) с
помощью функции sem_init(3). Первый параметр функции sem_init() – указатель на
переменную типа sem_t, которая служит идентификатором семафора. Второй
параметр - pshared - в настоящее время не используется, и мы оставим его
равным нулю. В третьем параметре функции sem_init() передается значение,
которым инициализируется семафор. Дальнейшая работа с семафором
осуществляется с помощью функций sem_wait(3) и sem_post(3). Единственным
аргументом функции sem_wait() служит указатель на идентификатор семафора.
Функция sem_wait() приостанавливает выполнение вызвавшего ее потока до тех
пор, пока значение семафора не станет большим нуля, после чего функция
уменьшает значение семафора на единицу и возвращает управление. Функция
sem_post() увеличивает значение семафора, идентификатор которого был передан
ей в качестве параметра, на единицу. Присвоив семафору значение 0, наша
программа создает первый поток и вызывает функцию sem_wait(). Эта функция
приостановит выполнение функции main() до тех пор, пока функция потока не
вызовет функцию sem_post(), а это случится только после того как функция потока
обработает значение переменной id. Таким образом, мы можем быть уверены, что
в момент создания второго потока первый поток уже закончит работу с
переменной id, и мы сможем использовать эту переменную для передачи данных
второму потоку. После завершения обоих потоков мы вызываем функцию
sem_destroy(3) для удаления семафора и высвобождения его ресурсов.
27.
Семафоры – не единственное средство синхронизации потоков. Для
разграничения доступа к глобальным объектам потоки могут использовать
мьютексы. Все функции и типы данных, имеющие отношение к мьютексам,
определены в файле pthread.h. Мьютекс создается вызовом функции
pthread_mutex_init(3). В качестве первого аргумента этой функции передается
указатель на переменную pthread_mutex_t, которая играет роль идентификатора
нового мьютекса. Вторым аргументом функции pthread_mutex_init() должен быть
указатель на переменную типа pthread_mutexattr_t. Эта переменная позволяет
установить дополнительные атрибуты мьютекса. Если нам нужен обычный
мьютекс, мы можем передать во втором параметре значение NULL. Для того
чтобы получить исключительный доступ к некоему глобальному ресурсу, поток
вызывает функцию pthread_mutex_lock(3), (в этом случае говорят, что «поток
захватывает мьютекс»). Единственным параметром функции pthread_mutex_lock()
должен быть идентификатор мьютекса. Закончив работу с глобальным ресурсом,
поток высвобождает мьютекс с помощью функции pthread_mutex_unlock(3),
которой также передается идентификатор мьютекса. Если поток вызовет функцию
pthread_mutex_lock() для мьютекса, уже захваченного другим потоком, эта
функция не вернет управление до тех пор, пока другой поток не высвободит
мьютекс с помощью вызова pthread_mutex_unlock() (после этого мьютекс,
естественно, перейдет во владение нового потока). Удаление мьютекса
выполняется с помощью функции pthread_mutex_destroy(3). Стоит отметить, что в
отличие от многих других функций, приостанавливающих работу потока, вызов
pthread_mutex_lock() не является точкой останова. Иначе говоря, поток,
находящийся в режиме отложенного досрочного завершения, не может быть
завершен в тот момент, когда он ожидает выхода из pthread_mutex_lock().
28.
Создавая новый поток, вы можете указать ряд дополнительных атрибутов,
определяющих некоторые его параметры. Из всех этих атрибутов более всего
востребован атрибут DETACHED, позволяющий создавать отделенные потоки. Во
всех рассмотренных выше примерах мы использовали функцию pthread_join(),
позволяющую дождаться завершения потока и получить значение, возвращенное
его функцией. Для того чтобы функция pthread_join() могла получить значение
функции потока, завершившегося до вызова pthread_join(), система сохраняет
данные о потоке после его завершения (это похоже на появления «зомби» после
завершения самостоятельного процесса). Если наша программа интенсивно
работает с потоками и синхронизация потоков с помощью pthread_join() нам не
нужна, мы можем сэкономить ресурсы системы, используя отделенные потоки.
Отделенные потоки отличаются от обычных (присоединяемых) потоков тем, что
после завершения отделенного потока система не сохраняет информацию о нем.
Если вызвать функцию pthread_join() для отделенного потока, она вернет
сообщение об ошибке.
Вы можете превратить присоединяемый поток в отделенный с помощью вызова
функции pthread_detach(3), однако придать потоку свойство «отделенности»
можно и на этапе его создания, с помощью дополнительного атрибута DETACHED.
Для того чтобы назначить потоку дополнительные атрибуты, нужно сначала
создать объект, содержащий набор атрибутов. Этот объект создается функцией
pthread_attr_init(3). Единственный аргумент этой функции – указатель на
переменную типа pthread_attr_t, которая служит идентификатором набора
атрибутов. Функция pthread_attr_init() инициализирует набор атрибутов потока
значениями, заданными по умолчанию, так что мы можем модифицировать только
те атрибуты, которые нас интересуют, и не беспокоиться об остальных. Для
добавления атрибутов в набор используются специальные функции с именами
pthread_attr_set<имя_атрибута>. Например, для того, чтобы добавить атрибут
«отделенности», мы вызываем функцию pthread_attr_setdetachstate(3). Первым
аргументом этой функции должен быть адрес объекта набора атрибутов, а вторым
аргументом – константа, определяющая значение атрибута. Константа
PTHREAD_CREATE_DETACHED указывает, что создаваемый поток должен быть
отделенным, тогда как константа PTHREAD_CREATE_JOINABLE определяет
создание присоединяемого (joinable) потока, который может быть
синхронизирован функций pthread_join(3). После того, как мы добавили
необходимые значения в набор атрибутов потока, мы вызываем функцию создания
потока pthread_create(). Набор атрибутов потока передается в качестве второго
аргумента этой функции.
30.
Каждый процесс-демон создает так называемый 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;
Стоит отметить, что теперь наш демон получил новый 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();
Этот вызов создает новую группу процессов, идентификатором которой
является идентификатор текущего процесса.
31.
Структура termios позволяет управлять флагами и численными
параметрами, которые можно разделить на пять групп: ввод, вывод,
управление оборудованием, локальные параметры и специальные
управляющие символы. Простейшая структура termios состоит из пяти полей,
соответствующих перечисленным группам:
struct termios {
tcflag_t c_iflag; // флаги управления вводом
tcflag_t c_oflag; // флаги управления выводом
tcflag_t c_cflag; // флаги управления оборудованием
tcflag_t c_lflag; // флаги управления локальными параметрами
cc_t c_cc[NCCS] // Специальные управляющие символы
};
У структуры termios могут быть и другие поля, но нас они не интересуют.
Обычно работа со структурой termios происходит по следующему сценарию
(все необходимые функции и типы данных определены в файле termios.h): с
помощью функции tcgetattr(3) мы получаем копию структуры, описывающую
текущее состояние терминала и делаем еще одну копию. Затем мы
модифицируем значения полей одной из копий termios, так чтобы изменить
нужные нам параметры терминала, и передаем системе новое значение
termios с помощью функции tcsetattr(3). После того, как работа с терминалом
в измененном режиме закончена, мы восстанавливаем исходное состояние
терминала с помощью сохраненной копии исходной структуры termios и
функции tcsetattr(). Первым аргументом функции tcgetattr() должен быть
дескриптор файл, соответствующего терминалу. Вторым аргументом является
указатель на структуру termios, в которой функция возвращает текущие
настройки терминала. Первым параметром функции tcsetattr() также служит
дескриптор файла терминала. Второй параметр используется для передачи
флагов, определяющих, когда изменения параметров терминала должны
вступить в силу. Третьим параметром tcsetattr() является указатель на
структуру termios, содержащую новые параметры.
Ключевой момент во всем этом, – модификация полей структуры termios.
Первые четыре поля структуры содержат комбинации флагов, определяющих
параметры терминала. Пятое поле представляет собой массив значений.
Индексам этого массива соответствуют специальные константы, с помощью
которых мы можем понять значение элементов массива. Рассмотрим сначала
поля termios, содержащие флаги. Полное описание флагов (а их довольно
много) вы найдете на странице man, посвященной termios. Я перечислю здесь
только некоторые флаги, которые устанавливаются в поле c_lflag, поскольку
они представляются мне наиболее интересными. Флаг ECHO управляет
отображением вводимых символов на экране монитора. Если он установлен,
символы отображаются, в противном случае – нет. Флаг ECHOE делает то же,
что и флаг ECHO, но только для управляющих символов, стирающих другие
символы или строки (например, BackSpace). Поскольку неканонический
режим не поддерживает редактирование строки, в этом режиме флаг ECHOE
игнорируется. Если установлен флаг ICANON, терминал находится в
каноническом режиме, в противном случае – в неканоническом. Флаг IEXTEN
переводит терминал в режим расширенной обработки вводимых символов. От
того, установлен ли флаг ISIG, зависит, будут ли специальные комбинации
клавиш, такие как Ctrl-C и Ctrl-Z, инициировать соответствующие им сигналы.
32.
#include < stdio.h>
#include < stdlib.h>
#include < signal.h>
#include < termios.h>
#define BUF_SIZE 15
int main (int argc, char ** argv)
{ struct termios oldsettings, newsettings;
char password[BUF_SIZE+1];
int len;
sigset_t newsigset, oldsigset;
sigemptyset(&newsigset);
sigaddset(&newsigset, SIGINT);
sigaddset(&newsigset, SIGTSTP);
sigprocmask(SIG_BLOCK, &newsigset, &oldsigset);
tcgetattr(fileno(stdin), &oldsettings);
newsettings = oldsettings;
newsettings.c_lflag &= ~ECHO;
tcsetattr(fileno(stdin), TCSAFLUSH, &newsettings);
printf("Enter password and press [Enter]\n");
len = read(fileno(stdin), password, BUF_SIZE);
password[len] = 0;
tcsetattr(fileno(stdin), TCSANOW, &oldsettings);
sigprocmask(SIG_SETMASK, &oldsigset, NULL);
printf("Your password is %s\n", password);
return EXIT_SUCCESS;
}
В начале программы мы блокируем сигналы SIGINT и SIGTSTP (зачем это
нужно, я объясню ниже). Затем, с помощью функции tcgetattr() мы заполняем
переменную oldsettings типа struct termios текущими значениями параметров
терминала. Далее мы копируем содержимое oldsettings в переменную
newsettings. Строка
newsettings.c_lflag &= ~ECHO;
Сбрасывает флаг ECHO в структуре newsettings. Остальные параметры
терминала остаются без изменений. Далее, с помощью функции tcsetattr() мы
устанавливаем новые параметры. Теперь терминал не будет выводить на
экран символы, вводимые пользователем, и мы можем вызвать функцию,
считывающую значение пароля. После этого программа восстанавливает
прежнее состояние терминала. Теперь мы можем разблокировать
заблокированные сигналы. Мы распечатываем строку с введенным «паролем»
(не вздумайте вводить в программе какой-нибудь настоящий пароль, иначе
злоумышленник, прячущийся за вашей спиной, обязательно его увидит).
Зачем мы блокировали сигналы вона время ввода пароля? Представьте
себе, что в то время, когда программа ожидает ввода пароля, пользователь
передумал и захотел завершить ее с помощью Ctrl-C. Если программа
завершится в этот момент, состояние терминала не будет восстановлено, и
символы, вводимые пользователем, по-прежнему не будут отображаться. Это
не смертельно, но неудобно. Вот почему программы, ожидающие ввода
пароля, временно блокируют некоторые сигналы.
34.
Основными концепциями пользовательского интерфейса программы,
использующей ncurses, являются экран (screen), окно (window) и под-окно
(sub-window). Экраном называется все пространство, на котором ncurses
может выводить данные. С точки зрения ncurses, экран – это матрица ячеек, в
которые можно выводить символы. Если монитор работает в текстовом
режиме, экран ncurses совпадает с экраном монитора. Если терминал
эмулируется графической программой, экраном является рабочая область
окна этой программы. Окном ncurses называется прямоугольная часть экрана,
для которой определены особые параметры вывода. В частности, размеры
окна влияют на перенос и прокрутку строк, выводимых в этом окне. В каком-
то смысле окно можно назвать «экраном в экране». На уровне интерфейса
программирования окна представлены структурами данных, по этой причине
мы будем часто говорить об окне как о структуре.
В процессе инициализации ncurses автоматически создается окно stdscr,
размеры которого совпадают с размерами экрана. Кроме структуры stdscr по
умолчанию создается еще одна структура – curscr. Операции вывода данных
ncurses модифицируют содержимое структуры stdscr, однако на экране всегда
отображается содержимое окна curscr. Иначе говоря, данные, которые
выводит ваша программа в окно stdscr (или в другое окно), не отображаются
на экране монитора автоматически. Для того чтобы сделать результаты
вывода видимыми, вы должны вызывать специальные функции обновления
экрана (refresh() или wrefresh()). Эти функции сравнивают содержимое окон
stdscr и curscr и на основе различий между ними вносят изменения в
структуру curscr, а затем обновляют экран. Благодаря наличию окна curscr,
ncurses-программе не требуется «помнить» весь свой предыдущий вывод и
перерисовывать его всякий раз, когда в этом возникает необходимость. Этим
программы ncurses отличаются от графических программ. В старину, когда
терминалы связывались с компьютерами через модемы, использование двух
окон давало дополнительное преимущество в скорости обмена данными, ведь
программе нужно было передавать на терминал не копию экрана целиком, а
только разницу между содержимым окон curscr и stdscr.
Хотя ваша программа может пользоваться для вывода данных
исключительно окном stdscr, ваша задача по проектированию интерфейса
существенно упростится, если вы будете создавать собственные окна,
расположенные «внутри» stdscr. Программа, использующая ncurses, может
работать с несколькими окнами одновременно, выполняя вывод в каждое из
них. Кроме окон (windows) программы ncurses могут создавать под-окна
(subwindows), поведение которых несколько отличается от поведения
стандартных окон.
Важнейшей особенностью ncurses является возможность указать
произвольную позицию курсора для вывода (и ввода) данных. Позиция
курсора отсчитывается от левого верхнего угла текущего окна. Ячейка в
верхнем левом углу имеет координаты (0, 0). При работе с функциями ncurses
важно помнить, что первой координатой является номер строки (что
соответствует y, в терминах графического программирования), а второй
координатой – номер столбца (что соответствует x в графическом режиме).
В случае ошибки функции ncurses обычно возвращают константу ERR. Если
функция не должна возвращать какое-то информативное значение (как,
например, функция getch()), в случае успешного выполнения она возвращает
значение OK.
35.
Написание первой программы ncurses (она называется cursed, исходный
текст вы найдете в файле cursed.c) мы начнем с перечисления заголовочных
файлов.
#include < termios.h>
#include < sys/ioctl.h>
#include < signal.h>
#include < stdlib.h>
#include < curses.h>
Помимо уже знакомых нам заголовочных файлов, в программу включен
файл , который содержит объявления функций, переменных,
констант и структур данных, экспортируемых библиотекой ncurses.
Прежде чем переходить к программированию ncurses, следует рассмотреть
решение одной задачи, с которой в настоящее время сталкиваются все
разработчики, использующие эту библиотеку. Речь идет об изменении
размеров окна терминала (под размерами окна в данном случае понимается
число строк и столбцов). Пользователи настоящих текстовых терминалов
редко переключали их режимы, и готовы были мириться с последствиями
своих действий. В наши дни, когда экраном терминала зачастую служит окно
графической программы, пользователь вправе ожидать, что при изменении
размеров окна работа консольной программы не нарушится, а ее интерфейс
не развалится.
Когда размеры окна терминала меняются, выполняющаяся в нем
программа получает сигнал SIGWINCH. Это одновременно и хорошо и плохо.
Хорошо – потому, что терминал информирует программу об изменении своих
размеров, плохо – потому, что сигналы имеют особенность вмешиваться в
работу программы. Например, если вы напишете программу, использующую
ncurses, и не позаботитесь об обработке сигнала SIGWINCH, при изменении
размеров окна терминала ваша программа может неожиданно завершиться,
оставив терминал в неканоническом состоянии. Рассмотрим, как
обрабатывается сигнал SIG_WINCH в программе cursed.
void sig_winch(int signo)
{ struct winsize size;
ioctl(fileno(stdout), TIOCGWINSZ, (char *) &size);
resizeterm(size.ws_row, size.ws_col);
}
Функция sig_winch() представляет собой обработчик сигнала SIGWINCH.
Следует отметить, что изменение размеров окна программы, работающей в
текстовом режиме, представляет собой довольно нетривиальную задачу и
стандартного рецепта, описывающего, что должна делать программа, когда
размеры окна изменились, не существует. Разработчики ncurses, как могли,
постарались упростить решение этой задачи для программистов, введя
функцию resizeterm(). Функцию resizeterm() следует вызывать сразу после
изменения размеров окна терминала. Аргументами функции resizeterm()
должны быть новые размеры экрана, заданные в строках и столбцах. Функция
resizeterm() старается сохранить внешний вид и порядок работы приложения
в изменившемся окне терминала, но это ей удается не всегда, с чем мы
столкнемся ниже. Необходимые для resizeterm() значения размеров окна мы
получаем с помощью специального вызова ioctl(). При этом первым
параметром функции ioctl() должен быть дескриптор файла устройства,
представляющего терминал. Вторым параметром ioctl() является константа
TIOCGWINSZ, а третьим параметром – адрес структуры winsize. Структура
winsize определенная в файле , включает в себя поля ws_row и
ws_col, в которых возвращается число строк и столбцов окна терминала.
Перейдем теперь к функции main() программы cursed:
int main(int argc, char ** argv)
{ initscr();
signal(SIGWINCH, sig_winch);
cbreak();
noecho();
curs_set(0);
attron(A_BOLD);
move(5, 15);
printw("Hello, brave new curses world!\n");
attroff(A_BOLD);
attron(A_BLINK);
move(7, 16);
printw("Press any key to continue...");
refresh();
getch();
endwin();
exit(EXIT_SUCCESS);
}
Работа с ncurses начинается с вызова функции initscr(). Эта функции
инициализирует структуры дынных ncurses и переводит терминал в нужный
режим. По окончании работы с ncurses следует вызвать функцию endwin(),
которая восстанавливает то состояние, в котором терминал находился до
инициализации ncurses. После вызова initscr() мы устанавливаем обработчик
сигнал SIGWINCH. Устанавливать обработчик SIGWINCH следует только
после инициализации ncurses, поскольку в обработчике используется функция
resizeterm(), предполагающая, что библиотека ncurses уже инициализирована.
Функция noecho() отключает отображение символов, вводимых с клавиатуры.
Функция cur_set() управляет видимостью курсора. Если вызвать эту функцию с
параметром 0, курсор станет невидимым, вызов же функции с ненулевым
параметром снова «включает» курсор.
Функция attron() позволяет указать некоторые дополнительные атрибуты
выводимого текста. Этой функции можно передать одну или несколько
констант, обозначающих атрибуты (в последнем случае их следует объединить
с помощью операции «|»). Например, атрибут A_UNDERLINE включает
подчеркивание текста, атрибут A_REVERSE меняет местами цвет фона и
текста, атрибут A_BLINK делает текст мигающим, атрибут A_DIM снижает
яркость текста по сравнению с нормальной, атрибут A_BOLD делает текст
жирным в монохромном режиме и управляет яркостью цвета в цветном
режиме работы монитора. Специальный атрибут COLOR_PAIR() применяется
для установки цветов фона и текста. На странице man, посвященной функции
attron(), вы найдете описания и других атрибутов. Все перечисленные выше
атрибуты оказывают воздействие только на тот текст, который выводится
после установки атрибута. Выше мы говорили о том, что окно ncurses
представляет собой матрицу ячеек для вывода символов. Помимо кода
символа каждая ячейка содержит дополнительные атрибуты символа.
Сбросить атрибуты можно с помощью функции attroff(). Так же, как и в случае
с attron(), функции attroff() можно передать несколько констант,
обозначающих атрибуты, разделенных символом «|». Так же, как и установка
атрибута, сброс атрибута влияет только на текст, напечатанный после сброса
(текст, напечатанный ранее с установленным атрибутом, остается без
изменений). В нашей программе мы сначала устанавливаем атрибут A_BOLD.
Теперь, до тех пор, пока мы не сбросим этот атрибут, весь текст будет
печататься жирным шрифтом. Прежде чем напечатать строку текста этим
шрифтом, мы воспользуемся еще одной возможностью ncurses – выводом
текста в произвольной области экрана. Функция move() устанавливает
позицию курсора в окне stdscr. Первый аргумент функции – строка, второй
аргумент – столбец, в котором должен находиться курсор. Следующий затем
вывод текста начнется с той позиции, в которой был установлен курсор. Если
попытаться поместить курсор за пределы окна, функция move() не станет
выполнять никаких действий, и курсор останется на прежнем месте. Мы
переводим курсор в позицию (5, 15) и выводим на экран строку "Hello, brave
new curses world!" с помощью функции printw(). Функция printw()
представляет собой аналог printf() для окна stdscr и имеет тот же список
параметров, что и printf().
Теперь мы сбрасываем атрибут A_BOLD с помощью функции attroff(),
устанавливаем атрибут A_BLINK, переводим курсор в позицию (7,16) и
распечатываем строку "Press any key to continue...". Хотя мы уже напечатали
две строки, на экране терминала все еще ничего нет. Для того чтобы
напечатанные нами символы стали видимыми, необходимо вызывать функцию
refresh(). Функция refresh() является, в некотором роде, избыточной
(действительно, почему бы не отображать распечатанный текст сразу же
после вызова printw()?). Фактически функция refresh() представляет собой
пережиток тех времен, когда терминал связывался с компьютером при
помощи модема. Контролируя частоту вызовов refresh(), можно было
сократить трафик между терминалом и компьютером (см. выше описание
взаимодействия окон stdscr и curscr).
36.
int main(int argc, char ** argv)
{ WINDOW *
wnd;
WINDOW * subwnd;
initscr();
signal(SIGWINCH, sig_winch);
cbreak();
curs_set(0);
refresh();
wnd = newwin(6, 18, 2, 4);
box(wnd, '|', '-');
subwnd = derwin(wnd, 4, 16, 1, 1);
wprintw(subwnd, "Hello, brave new curses world!\n");
wrefresh(wnd);
delwin(subwnd);
delwin(wnd);
move(9, 0);
printw("Press any key to continue...");
refresh();
getch();
endwin();
exit(EXIT_SUCCESS);
}
В функции main() мы, как и прежде, инициализируем ncurses с помощью
функции initscr() и устанавливаем обработчик SIGWINCH. Далее мы
делаем курсор невидимым, как и в предыдущем примере. После этого
мы должны обновить экран с помощью refresh().
Мы создаем новое окно с помощью функции newwin(). Наше окно
насчитывает 6 строк и 18 столбцов и его верхний левый угол находится в
ячейке (2, 4) окна stdscr. Указатель на структуру WINDOW, который
возвращает функция newwin(), мы сохраняем в переменной wnd. Функция
box(), которую мы вызываем далее, позволяет создать рамку вдоль границы
окна. Аргументами этой функции должны быть идентификатор окна и
символы, используемые, соответственно, для рисования вертикальной и
горизонтальной границы. Теперь было бы логично вывести какой-нибудь текст
в окно, обрамленное рамкой, но тут возникает одна сложность. Поскольку
символы рамки сами находятся внутри окна, символы текста могут затереть
их в процессе вывода. Мы решаем эту проблему с помощью создания под-окна
subwnd внутри окна wnd и вывода текста в это под-окно. Поскольку окно
subwnd по размерам меньше, чем окно wnd, символы рамки не будут стерты.
Теперь мы можем распечатать текст, что мы и делаем с помощью функции
wprintw(), указав ей идентификатор окна subwnd. Для того чтобы символы,
напечатанные в окне, стали видимыми, мы должны вызвать функцию
wrefresh(). Мы вызываем эту функцию только для окна wnd, поскольку оно
содержит символьный массив и своего под-окна subwnd. Обратите внимание
на то, что символы строки "Hello, brave new curses world!", которую мы
печатаем в под-окне subwnd с помощью функции wprintw(), переносятся при
достижении границы под-окна (рис. 2). После завершения работы с окнами
мы можем удалить структуры wnd и subwnd с помощью функции delwin().
Весь вывод, выполненный в окне wnd, останется на экране (точнее в окне
curscr) до тех пор, пока вы не перезапишете его другим выводом.
37.
Библиотека ncurses инициализирует восемь базовых цветов: черный, красный, зеленый,
желтый, синий (blue), ярко-красный (magenta), голубой (cyan) и белый
(базовыми называются цвета с обычным уровнем яркости). Поскольку к
каждому базовому цвету можно применить атрибут повышенной яркости
A_BOLD, мы получаем всего 16 цветов (в результате применения атрибута
A_BOLD к черному цвету получается темно-серый цвет). Базовым цветам
соответствуют константы COLOR_BLACK, COLOR_RED, COLOR_GREEN,
COLOR_YELLLOW, COLOR_BLUE, COLOR_MAGENTA, COLOR_CYAN и
COLOR_WHITE (для черного, красного, зеленого, желтого, синего, ярко-
красного, голубого и белого цветов соответственно). Следует отметить, что
фактические цвета в окне терминала зависят, прежде всего, от настроек
самого терминала. Например, базовый желтый цвет (COLOR_YELLLOW) будет
выглядеть скорее как коричневый, а для того, чтобы он стал, собственно,
желтым, ему необходимо придать атрибут повышенной яркости. Библиотека
ncursese позволяет определять собственные цвета с помощью функции
init_color, но эта возможность поддерживается не всеми консолями.
Позволяет ли консоль определять собственные цвета, можно выяснить с
помощью функции can_change_color(). Цвета в ncurses объединяются в пары –
цвет символов (foreground) и цвет фона (background). Перед тем как печатать
цветной текст, необходимо определить соответствующую цветовую пару и
установить ее в качестве атрибута текста (так же как устанавливается атрибут
мигания или подчеркивания). Фактически номер цветовой пары является
одним из атрибутов символа. Изменить цвет фона или цвет символов
независимо друг от друга нельзя, необходимо определять новую пару.
Система управления цветами ncurses инициализирует две переменные –
COLORS (количество базовых цветов) и COLOR_PAIRS (максимальное
количество цветовых пар, которые можно определить одновременно). При
работе с терминалом konsole эти переменные принимают значения 8 и 64
соответственно.
38.
Еще одной важной возможностью, которую ncurses предоставляет
программистам, является поддержка мыши в окне терминала. Рассмотрим
программу cursedmouse (на диске – файл cursedmouse.c), которая
регистрирует щелчки левой кнопкой мыши, сделанные пользователем в окне
терминала, и распечатывает координаты курсора мыши в момент щелчка.
Ради простоты мы не создаем в этой программе никаких окон (кроме окна
stdscr, которое создается автоматически).
#include < termios.h>
#include < sys/ioctl.h>
#include < signal.h>
#include < stdlib.h>
#include < curses.h>
void sig_winch(int signo)
{ struct winsize size;
ioctl(fileno(stdout), TIOCGWINSZ, (char *) &size);
resizeterm(size.ws_row, size.ws_col);
nodelay(stdscr, 1);
while (wgetch(stdscr) != ERR);
nodelay(stdscr, 0);
}
int main(int argc, char ** argv)
{ initscr();
signal(SIGWINCH, sig_winch);
keypad(stdscr, 1);
mousemask(BUTTON1_CLICKED, NULL);
move(2,2);
printw("Press the left mouse button to test mouse\n");
printw("Press any key to quit...\n");
refresh();
while (wgetch(stdscr) == KEY_MOUSE) {
MEVENT event;
getmouse(&event);
move(0, 0);
printw("Mouse button pressed at %i, %i\n", event.x, event.y);
refresh();
move(event.y, event.x);
}
endwin();
exit(EXIT_SUCCESS);
}
Поддержка мыши в ncurses инициализируется с помощью функции
mousemask(). Первым параметром этой функции должна быть маска событий
мыши, которые следует обрабатывать в программе, вторым параметром может
быть указатель на переменную, в которой функция сохранит прежнюю маску
событий или NULL, если прежняя маска нам не нужна. Каждому событию
мыши в ncurses соответствует константа. Если мы хотим обрабатывать
несколько событий мыши, при вызове функции mousemask() мы должны
объединить соответствующие константы операцией «|». Повторный вызов
mousemask() приведет к установке новой маски событий (вызов mousemask() с
первым аргументом, равным 0, отключает поддержку мыши).
Рассмотрим некоторые константы, определяющие события мыши.
Константа BUTTON1_CLICKED соответствует щелчку левой кнопкой мыши
(точнее говоря, - щелчку первой кнопкой; будет ли первая кнопка левой
кнопкой мыши, зависит от настроек мыши). Константа BUTTON2_PRESSED
указывает, что программа должна реагировать на нажатие пользователем
второй (обычно – правой) кнопки мыши. Константа
REPORT_MOUSE_POSITION указывает, что мы хотим отслеживать движение
указателя мыши, а константа ALL_MOUSE_EVENTS заставляет программу
реагировать на все события мыши (более полное описание констант событий
вы найдете на странице man функции mousemask(3x)). В качестве
результирующего значения функция mousemask() возвращает маску из
выбранных нами событий, которые фактически могут быть обработаны. Если
функция возвращает 0, значит работа с мышью в консоли не поддерживается.
Каждый раз, когда в системе происходит одно из «наблюдаемых» событий
мыши, в потоке ввода программы появляется специальный символ
KEY_MOUSE. Точнее говоря, по умолчанию, в потоке ввода программы Linux
появляется Esc-последовательность, соответствующая этому символу, так что
в программе cursedmouse мы тоже должны вызвать функцию keypad() с
ненулевым вторым параметром.
После того, как мы считали из потока ввода специальный символ
KEY_MOUSE, мы можем получить более подробную информацию о вызвавшем
его событии мыши. Делается это с помощью функции getmouse(). Аргументом
функции getmouse() должен быть указатель на структуру MEVENT.
Определение структуры MEVENT выглядит следующим образом:
typedef struct {
short id; /* идентификатор для различения нескольких устройств */
int x, y, z; /* координаты указателя в момент событи */
mmask_t bstate; /* маска событий */
} MEVENT;
Координаты указателя возвращаются в формате строка (y), столбец (x).
Поле bstate содержит один единственный бит, соответствующий константе
события.
В программе cursedmouse мы считываем поступающие во входной поток
символы в цикле. Если во входном потоке появляется символ KEY_MOUSE,
мы, с помощью функции getmouse(), определяем координаты указателя мыши
в момент события и распечатываем их (мы распечатываем строку с
координатами в левом верхнем углу экрана, а затем переводим курсор туда,
куда указывала мышь в момент события). Появление в потоке ввода символа,
отличного от KEY_MOUSE, приводит к завершению программы.
Осталось обратить внимание читателя на обработку сигнала SIGWINCH в
программе cursedmouse. Изменение размеров экрана при включенной
поддержке мыши приведет к появлению в потоке ввода символов Esc-
последовательности специального символа KEY_RESIZE (это еще один способ
предупредить программу о том, что размеры экрана изменились). В
программе cursedmouse появление в потоке ввода каких-либо кодов,
отличных от KEY_MOUSE, вызывает завершение программы. Для того чтобы
избежать досрочного завершения, в обработчике сигнала SIGWINCH мы
опустошаем поток ввода с помощью функции flushinp(). Естественно, этот
способ спасения программы от досрочного завершения годится не всегда
(ведь в момент изменения размеров окна терминала поток ввода может
содержать важную информацию). Все это лишний раз демонстрирует,
насколько нетривиальной является обработка изменения размеров экрана в
программах ncurses.
1. Выполняет системный вызов из libc . Имеет не более 5 параметров .
Возвращает -1 в случае неудачи и >=0 в случае удачи.
36. termcap - таблица элементов описания работы с терминалом в ASCII-файле /etc/termcap.
Здесь вы можете найти информацию о том, как выводить специальные символы,
как осуществлять операции (удаления, вставки символов или строк и т.д.) и как инициализировать терминал.
Имеются библиотечные функции для чтения и использования возможностей терминала (смотри termcap(3x)).
База данных TERMinal INFOrmation построена над базой данных termcap и описывает некоторые возможности
терминалов на более высоком уровне.
С terminfo программа может легко менять атрибуты экрана, используя специальные клавиши,
такие как функциональные клавиши и др.
Эта база данных может быть найдена в /usr/.../terminfo/[A-z,0-9]*.
Библиотека (BSD-)curses дает вам высокоуровневый доступ к терминалу, базируясь на terminfo.
curses позволяет открывать и манипулировать окнами на экране,
предоставляет весь необходимый набор функций ввода/вывода.
ncurses представляет собой развитие curses. В версии 1.8.6 она должна быть совместима с AT&T curses, как это определено в SYSVR4, и иметь несколько расширений, таких как манипулирование цветами, специальная оптимизация для вывода, оптимизации, зависящие от терминала, и др.
38.
Создание окна :
WINDOW *newwin(nlines, ncols, begy, begx)
WINDOW *mywin;
mywin=newwin(0,0,0,0);
Удаление окна :
int delwin(win)
Перемещение окна :
int mvwin(win, by, bx)
Рисует вложенное окно поверх в центре
WINDOW *subwin(origwin, nlines, ncols, begy, begx)
Дублирование окна :
WINDOW *dupwin(win)
Копирование текста из одного окна в другое без пропусков :
int overlay(win1, win2)
Копирование текста из одного окна в другое с пропусками :
int overwrite(win1, win2)
Копирование части окна
int copywin(win1, win2, sminrow, smincol, dminrow, dmincol,
39.
Символьные
int addch(ch)
int waddch(win, ch)
int mvaddch(y, x, ch)
int mvwaddch(win, y, x, ch)
Строковые
int addstr(str)
int addnstr(str, n)
int waddstr(win, str)
int waddnstr(win, str, n)
int mvaddstr(y, x, str)
int mvaddnstr(y, x, str, n)
int mvwaddstr(win, y, x, str)
int mvwaddnstr(win, y, x, str, n)
2.
Первые три файловых дескриптора для процессов 0, 1 и 2 имеют стандартное
назначение. Первый, 0, известен как стандартный ввод (stdin) и является местом, откуда
программы должны получать свой интерактивный ввод. Файловый дескриптор 1
называется стандартным выводом {stdout), и большая часть вывода программ должна быть
направлена в него. Сообщения об ошибках должны направляться в стандартный поток
ошибок {stderr), который имеет файловый дескриптор 2. Стандартная библиотека С
следует этим правилам, поэтому gets() и printf () используют stdin и stdout
соответственно, и это соглашение дает возможность командным оболочкам правильно
перенаправлять ввод и вывод процессов.
Заголовочный файл < unistd.h> представляет макросы STDINFILENO, STDOUT_
FILENO и STDERRFILENO, которые вычисляются как файловые дескрипторы stdin,
stdout и stderr соответственно. Использование этих символических имен делает код более
читабельным.
Многие из файловых операций, которые манипулируют файловыми узлами inode,
доступны в двух формах. Первая форма принимает в качестве аргумента имя файла.
Ядро использует этот аргумент для поиска inode файла и выполняет соответствующую
операцию над ним (обычно это включает следование символическим ссылкам). Вторая
форма принимает файловый дескриптор в качестве аргумента и выполняет операцию
над inode, на который он ссылается. Эти два набора системных вызовов используют
похожие имена, но системные вызовы, работающие с файловыми дескрипторами, имеют
префикс/. Например, системный вызов chmod () изменяет права доступа для файла,
ссылка на который осуществляется по имени; f chmod () устанавливает права доступа к
файлу, ссылаясь на него по указанному файловому дескриптору.
4.
Файлы Unix можно разделить на две категории: просматриваемые (seekable) и не-
просматриваемые (nonseekable)8. Непросматриваемые файлы представляют собой
каналы, работающие в режиме "первый вошел— первый вышел" (FIFO), которые не
поддерживают произвольное чтение или запись, их данные не могут быть перечитаны
или перезаписаны. Просматриваемые файлы позволяют читать и писать в произвольное
место файла. Каналы и сокеты являются не просматриваемыми файлами; блоковые
устройства и обычные файлы являются просматриваемыми.
Поскольку FIFO — это непросматриваемые файлы, то, очевидно, что read () читает
их с начала, a write () пишет в конец. С другой стороны, просматриваемые файлы не
имеют очевидной точки для этих операций. Вместо этого здесь вводится понятие
"текущего" положения, которое перемещается вперед после операции. Когда
просматриваемый файл изначально открывается, текущее положение устанавливается в его начало,
со смещением 0. Если затем из него читается 10 байт, то текущее положение
перемещается в точку со смещением 10 от начала/
Файлы, открытые с флагом OAPPEND ведут себя несколько иначе. Для таких файлов
текущая позиция перемещается в конец файла перед тем, как ядро осуществит в него запись.
После записи текущая позиция перемещается в конец записанных данных, как обычно.
Для файлов, работающих в режиме только для добавления, это гарантирует, что текущая
позиция файла всегда будет находиться в его конце немедленно после вызова write ().
Приложения, которые хотят читать и писать данные с произвольного места файла,
должны установить текущую позицию перед выполнением операции чтения или записи
данных, используя lseek ().
int lseek(int fd, off_t offset, int whence);
Текущая позиция для файла fd перемещается на offset байт относительно whence,
где whence принимает одно из следующих значений:
SEEKSET10 Начало файла.
SEEK_CUR Текущая позиция в файле.
SEEKEND Конец файла.
Для SEEKCUR и SEEKEND значение offset может быть отрицательным. В этом
случае текущая позиция перемещается в сторону начала файла (от whence), а не в сторону
конца. Например, приведенный ниже код перемещает текущую позицию на 5 байт
назад от конца файла.
lseek(fd, -5, SEEK_END);
В большинстве систем POSIX процессам разрешается перемещать текущую позицию
за конец файла. При этом файл увеличивается до соответствующего размера и его
текущая позиция устанавливается в новый конец файла. Единственной ловушкой может
быть то, что большинство систем при этом не выделяют никакого дискового
пространства для той части файла, которая не была записана — они просто изменяют
логический размер файла.
Части файлов, которые "создаются" подобным образом, называют "дырками" (holes).
Чтение из такой "дырки" в файле возвращает буфер, полный двоичных нулей, а запись
в них может завершиться ошибкой по причине отсутствия свободного пространства на
диске. Все это значит, что вызов lseek () не должен применяться для резервирования
дискового пространства для позднейшего использования, поскольку такое
пространство может быть и не выделено. Если ваше приложение нуждается в выделении
некоторого дискового пространства для последующего использования, вы должны применять
write (). Файлы с "дырками" часто используют для хранения редко расположенных в
них данных, таких как файлы, представляющие хеш-таблицы.
8.
Когда множество имен файлов в файловой системе ссылаются на единственный
inode, такие файлы называют жесткими ссылками (hard links) на него. Все эти
имена должны располагаться на одном физическом носителе (обычно это значит, что они
должны быть на одном устройстве). Когда файл имеет множество жестких ссылок, все
они равны — нет способа узнать, с каким именем первоначально был создан файл. Одно
из преимуществ такой модели заключается в том, что удаление одной жесткой ссылки
не удаляет файл с устройства — он остается до тех пор, пока все ссылки на него не будут
удалены. Системный вызов link () связывает новое имя файла с существующим inode.
Символические ссылки — это более гибкий тип ссылок, чем жесткие, но они не
дают равноправного доступа к файлу, как это делают жесткие. В то время как жесткие
ссылки разделяют один и тот же inode, символические ссылки просто указывают на
другие имена файлов. Если файл, на который указывает символическая ссылка,
удаляется, то она указывает на несуществующий файл, что приводит к появлению висячих
ссылок. Использование символических ссылок между подкаталогами — обычная
практика, и они могут также пересекать границы физических систем, чего не могут жесткие
ссылки.
Символически ссылки создаются почти так же, как жесткие, но при этом
используется системный вызов symlink ().
int symlink(const char *origpath, const char *newpath);
9.
Иногда процессам требуется создать новый файловый дескриптор, который
ссылается на ранее открытый файл. Командные оболочки используют эту функциональность
для перенаправления стандартного ввода, вывода и потока ошибок по запросу
пользователя. Если процессу не важно, какой файловый дескриптор будет использован для
новой ссылки, он должен использовать dup ().
int dup(int oldfd);
dup () возвращает файловый дескриптор, который ссылается на тот же inode, что и
oldfd, или -1 в случае ошибки, oldfd остается корректным дескриптором,
по-прежнему ссылающимся на исходный файл. Новый файловый дескриптор — это всегда
наименьший доступный файловый дескриптор. Если процессу нужно получить новый
файловый дескриптор с определенным значением (например, 0, чтобы перенаправить
стандартный ввод), то он должен использовать dup2 ().
int dup2(int oldfd6 int newfd);
Если newf d ссылается на уже открытый дескриптор, то он закрывается. Если вызов
завершен успешно, он возвращает новый файловый дескриптор и newf d ссылается на
тот же файл, что oldfd. Системный вызов fcntl () представляет почти ту же
функциональность командой FDUPFLD. Первый аргумент — f d — это уже открытый файловый
дескриптор. Новый файловый дескриптор — это первый доступный дескриптор, равный
или больший, чем значение последнего аргумента fcntl (). (В этом состоит отличие от
работы dup2 ().) Вы можете реализовать dup2 () через fcntl () следующим образом.
int dup2(int oldfd, int newfd) {
close (newfd) ; /* ensure new fd is available */
return fcntl(oldfd, F_DUPFD, newfd);
}
Создание двух файловых дескрипторов, ссылающихся на один и тот же файл — это
не то же самое, что открытие файла дважды. Почти все атрибуты дублированных деск-
рипторов разделяются: они разделяют текущую позицию, режим доступа и блокировки.
(Все это записывается в файловой структуре15, которая создается при каждом открытии
файла. Файловый дескриптор ссылается на файловую структуру, и дескриптор,
возвращенный dup (), ссылается на одну и ту же структуру.) Единственный атрибут, который
может независимо управляться в этих двух дескрипторах — это состояние "закрыть при
выполнении". После того, как процесс вызывает fork (), то файлы, открытые в
родительском процессе, наследуются дочерним, и эти пары файловых дескрипторов (один в
родительском процессе, другой — в дочернем) ведут себя так, будто файловые
дескрипторы были дублированы с текущей позицией и другими разделенными атрибутами.
10.
Сигналы имеют четко определенный жизненный цикл: они создаются, сохраняются
до тех пор, пока ядро не выполнит определенное действие на основе сигнала, а затем
вызывают совершение этого действия. Создание сигнала называют по-разному:
поднятие (raising), генерация или посылка сигнала. Обычно процесс посылает сигнал другому
процессу, в то время как ядро генерирует сигналы для отправки процессу. Когда процесс
посылает сигнал самому себе, говорят, что он поднимает его. Однако эти термины
используются не особо согласованно.
Между временем, когда сигнал отправлен и тем, когда он вызывает какое-то
действие, его называют ожидающим (pending). Это значит, что ядро знает, что сигнал должен
быть обработан, но пока не имеет возможности сделать это. Как только сигнал
поступает в процесс назначения, он называется доставленным. Если доставленный сигнал
вызывает выполнение специального фрагмента кода (имеется в виду обработчик сигнала),
то такой сигнал считается перехваченным. Есть разные способы, которыми процесс
может предотвратить асинхронную доставку сигнала, но все же обработать его (например,
с помощью системного вызова sigwait ()). Когда такое случается, сигнал называют
принятым.
Изначально обработка сигналов была проста. Системный вызов signal ()
использовался для того, чтобы сообщить ядру, как доставить процессу определенный сигнал.
void * signal(int signum, void *handler);
13.
Когда программа построена преимущественно вокруг сигналов, часто
необходимо, чтобы она ожидала появления какого-то сигнала, прежде чем продолжать работу.
Системный вызов pause () предоставляет простую возможность для этого.
int pause(void);
Функция pause () не возвращает управления до тех пор, пока сигнал не будет
доставлен процессу. Если зарегистрирован обработчик для этого сигнала, то он запускается
до того, как pause () вернет управление, pause () всегда возвращает -1 и устанавливает
errno равным EINTR.
Системный вызов sigsuspend () предлагает альтернативный метод ожидания
вызова сигнала.
int sigsuspend(const sigset_t *mask);
Как и pause (), sigsuspend () временно приостанавливает процесс до тех пор, пока
не будет получен сигнал (и обработан связанным с ним обработчиком, если таковой
предусмотрен), возвращая -1 и устанавливая errno в EINTR.
В отличие от pause (), sigsuspend () временно устанавливает маску сигналов
процесса в значение, находящееся по адресу, указанному в mask, на период ожидания
появления сигнала. Как только сигнал поступает, маска сигналов восстанавливается в то
значение, которое она имела до вызова sigsuspend (). Это позволяет процессу ожидать
появления определенного сигнала за счет блокирования всех остальных сигналов12.
14.
Для эффективного мультиплексирования Linux предоставляет системный вызов
poll (), позволяющий процессу блокировать одновременно несколько файловых
дескрипторов. Постоянно проверяя каждый файловый дескриптор, процесс создает
отдельный системный вызов, определяющий, из каких файловых дескрипторов процесс будет
читать, а на какие — записывать. Когда один или несколько таких файлов имеют
данные, доступные для чтения, или могут принимать данные, записываемые в них, poll ()
завершается, и приложение может считывать и записывать данные в дескрипторах, не
беспокоясь о блокировке. После обработки этих файлов процесс создает еще один
вызов poll (), блокируемый до готовности файла. Ниже показано определение poll ().
int poll (struct pollfd * fds , int numfds , int timeout) ;
Последние два параметра очень просты; numf ds задает количество элементов в
массиве, на который указывает первый параметр, a timeout определяет, насколько долго
poll () должна ожидать события. Если в качестве таймаута задается 0, poll () никогда
не входит в состояние таймаута.
Первый параметр, f ds, описывает, какие файловые дескрипторы следует
контролировать, и для каких типов ввода-вывода. Это указатель на массив структур struct
pollfd.
struct pollfd
{
int fd;
short events;
short revents;
} ;
Первый элемент, f d, является контролируемым файловым дескриптором, а элемент
events описывает, какие типы событий подлежат мониторингу. Последний
представляет собой один или несколько перечисленных флагов, объединенных с помощью
логического "ИЛИ".
POLL IN Нормальные данные доступны для считывания из файлового дескриптора.
POLLPRI Приоритетные (внешние1) данные доступны для считывания.
POLLOUT Файловый дескриптор может принимать записываемые на него данные.
15.
Системный вызов poll () был изначально представлен как часть Unix-дерева
System V. Усилиями разработчиков BSD та же основная проблема была решена
похожим способом — предоставлением системного вызова seledt().
int select(int numfds, fd_set * readfds, fd_set * writefds,
fd__set * exceptfds, struct timeval * timeout) ;
Три промежуточных параметра — readfds, writefds и exceptfds — определяют, за
какими файловыми дескрипторами необходимо следить. Каждый параметр — это
указатель на f dset, структуру данных, позволяющую процессу определить произвольное
количество файловых дескрипторов2. Ею манипулируют с помощью перечисленных
ниже макросов.
FD_ZER0 (fd_set * fds); Очищает fds — в наборе не содержатся файловые
дескрипторы. Этот макрос используется для инициализации структур fd_set.
FD_SET(intfd, fd_set * fds); Добавляет f d к f dset.
FD CLR(intfd, fd set * fds) ; Удаляет f d из f d_set.
FD_ISSET (int fd, fd_set * fds); Возвращает true, если fd содержится в установленном fds.
Первый набор файловых дескрипторов select (), readfds, содержит перечень
файловых дескрипторов, вызывающих возврат вызова select (), когда они готовы для
чтения или (для каналов и сокетов) когда процесс на другом конце файла закрыл его.
Когда любой файловый дескриптор в writefds готов к записи, select () возвращается.
exceptfds содержит файловые дескрипторы для слежения за исключительными
условиями. В Linux (так же, как и в Unix) это происходит только при поступлении внешних
данных в сетевое подключение. В качестве любого из них можно указать NULL, если тот
или иной тип события вас не интересует.
Обладая одинаковой функциональностью, poll () и select () также имеют
существенные отличия. Наиболее очевидным отличием является таймаут,
поддерживающий миллисекундную точность для poll () и микросекундную точность для select ().
В действительности же это отличие почти незначительно, поскольку ни один
системный вызов не будет подготовлен с точностью до микросекунды.
Более важное отличие связано с производительностью. Интерфейс poll () обладает
несколькими свойствами, делающими его намного эффективнее, чем select ().
1. При использовании select () ядру необходимо проверить все файловые
дескрипторы между 0 и numf ds - 1, чтобы убедиться, заинтересовано ли приложение
в событиях ввода-вывода для этого файлового дескриптора. Для приложений с
большим количеством открытых файлов это может приэести к существенным
затратам, поскольку ядро проверяет, какие именно файловые дескрипторы
являются объектом интереса.
2. Набор файловых дескрипторов передается ядру как битовая карта для select () и
как список для poll (). Сложные битовые операции, необходимее для проверки
и установки структур данных f d set, менее эффективны, чем простые проверки,
требуемые для struct pollfd.
3. Поскольку ядро переписывает структуры данных, передаваемые select (),
приложение вынуждено сбрасывать эти структуры каждый раз перед вызовом select ().
С poll () результаты ядра ограничены элементом revents, что устраняет
потребность в восстановлении структур данных после каждого вызова.
4. Использование структуры, основанной на множествах (например, fdset) не
масштабируется по мере увеличения количества доступных процессу файловых
дескрипторов. Поскольку ее размер статичен, а не динамичен (обратите внимание
на отсутствие соответствующего макроса, например, FDFREE), она не может
расширяться или сжиматься в соответствии с потребностями приложения (или
возможностями ядра). В Linux максимальный файловый дескриптор, который можно
установить в f d_set, равен 1023. Если понадобится больший файловый
дескриптор, select () работать не будет.
16.
Следующая короткая программа, подсчитывающая количество системных вызовов в
секунду, демонстрирует, насколько poll () эффективнее select ().
int gotAlarm;
void catch(int sig)
{
gotAlarm = 1;
}
#define HIGH_FD 1000
int main(int argc, const char ** argv) {
int devZero;
int count;
fd_set select Fds;
struct pollfd pollFds;
devZero = open("/dev/zero", 0_RDONLY);
dup2(devZero, HIGH_FD);
/* с помощью signal выяснить, когда время истекло */
signal(SIGALRM, catch);
gotAlarm = 0;
count =0;
alarmA);
while (!gotAlarm)
{
FD_ZERO(&selectFds);
FD_SET(HIGH_FD, &selectFds);
select(HIGH_FD + 1, &selectFds, NULL, NULL, NULL);
count++;
}
printf("Вызовов select() в секунду: %d\n", count);
pollFds.fd = HIGH_FD;
pollFds.events = POLLIN;
count = 0;
gotAlarm =0;
alarm 1);
while (!gotAlarm)
{
poll(&pollFds, 0, 0);
count++;
}
printf("Вызовов poll() в секунду: %d\n", count);
return 0;
}
Здесь используется устройство /dev/zero, предоставляющее бесконечное
количество нулей, что обеспечивает немедленный возврат системных вызовов. Значение
HIGHFD можно изменить, чтобы посмотреть, как деградирует select () по мере роста
значений файловых дескрипторов.
В определенной системе при не очень высоком значении HIGHFD, равном 2,
программа показала, что ядро за секунду может обрабатывать в четыре раза больше
вызовов poll (), чем вызовов select (). При увеличении HIGHFD до 1000 эффективность
poll () становится в 40 раз выше, чем у select ().
18.
Функция getcwd () позволяет процессу найти имя своего текущего каталога
относительно корневого каталога системы.
char * getcwd(char * buf, size_t size);
Предусмотрено два системных вызова, меняющих текущий каталог процесса: chdir ()
и f chdir ().
int chdir(const char * pathname);
int fchdir(int fd) ;
Первый системный вызов получает имя каталога в качестве единственного
аргумента; второй принимает файловый дескриптор, являющийся открытым каталогом.
Хотя в системе имеется один корневой каталог, значение / может меняться для
каждого процесса в системе. Это обычно делается для предотвращения доступа к
файловой системе со стороны сомнительных процессов (например, демоны ftp,
обрабатывающие запросы ненадежных пользователей). Например, если в качестве корневого каталога
процесса определен /home/ftp, запуск chdir ("/") сделает текущий каталог процесса
/home/ftp, a getcwd () вернет / для поддержания последовательности данного процесса.
С целью обеспечения безопасности, если процесс пытается выполнить chdir ("/.."), он
остается в своем каталоге / (каталог /home/ftp в масштабах всей системы), так же как
и нормальные процессы, выполняющие chdir ("/..") остаются в корневом каталоге в
масштабах всей системы. Процесс может легко изменять свой текущий корневой каталог
с помощью системного вызова enroot (). Но путь нового корневого каталога
процесса интерпретируется с помощью текущего установленного корневого каталога, поэтому
chroot (" / ") не модифицирует текущий корневой каталог процесса.
int chroot(const char * path);
Создание новых каталогов выполняется очень просто.
int mkdir(const char * dirname, mode_t mode);
Удаление каталога — это практически то же, что и удаление файла; меняется разве
что имя системного вызова.
int rmdir(char * pathname);
Для успешного выпЬлнения rmdir () каталог должен быть пустым (он не
должен содержать ничего, кроме вездесущих . и . .); в противном случае возвращается
ENOTEMPTY.
Обычно программам требуется получать список файлов, содержащихся в каталоге.
Linux предоставляет ряд функций, позволяющих обрабатывать каталог как абстрактный
объект, что дает возможность избежать зависимости программ от точного формата
каталогов, реализуемого файловой системой. Открытие и закрытие каталогов
осуществляется очень просто.
DIR * opendir(const char * pathname);
int closedir(DIR * dir);
Системный вызов readdir () возвращает имя следующего файла в каталоге.
Каталоги не упорядочены каким-либо образом, поэтому не стоит предполагать, что
оглавление каталога отсортировано. Если необходим упорядоченный список файлов,
сортировку придется выполнять самостоятельно. Функция readdir () определяется, как
показано ниже.
struct dirent * readdir(DIR * dir);
Существуют две функции, которые облегчают приложениям просмотр всех файлов
каталога, включая файлы в подкаталогах. Рекурсивный просмотр всех элементов
древовидной структуры (например, файловой системы) часто называется обходом (walk)
дерева и он реализуется функциями f tw () и nf tw (). nf tw () представляет собой
усовершенствованную версию f tw.
int ftw (const char *dir, ftwFunctionPointer callback, int depth);
Функция ftw () начинает с каталога dir и вызывает указанную в callback
функцию для каждого файла в этом каталоге и его подкаталогах. Функция вызывается для
всех типов файлов, включая символические ссылки и каталоги.
19.
Команда find выполняет в одном или нескольких деревьях каталогов поиск файлов,
соответствующих определенным характеристикам. Ниже приведена простая
реализация find, реализованная на основе nftw (). Она использует fnmatch () (см. главу 23)
для реализации переключателя -name и иллюстрирует многие флаги, воспринимаемые
nftw ().
const char * name = NULL;
int minDepth = 0, maxDepth = INT_MAX;
int find (const char * file, const struct stat * sb, int flags,struct FTW * f)
{
if (f->level < minDepth) return 0;
if (f->level > maxDepth) return 0;
if (name && fnmatch(name, file + f->base, FNM_PERIOD)) return 0;
if (flags == FTW_DNR) {
fprintf(stderr, "find: %s: недопустимые полномочия\п", file);
} else {
printf("%s\n", file);
}
return 0;
}
int main(int argc, const char ** argv) {
int flags = FTWJPHYS;
int i;
int problem =0;
int tmp;
int re;
char * chptr;
/* поиск первого параметры командной строки (который должен находиться после списка путей */
i = 1;
while (i < argc && *argv[i] ! = '-') i ++;
/* обработать опции командной строки */
while (i < argc && !problem)
{
if (!stremp(argv[i],"-name"))
{
i++;
if (i == argc)
problem = 1;
else
name = argv[i++];
}
else if (Istrcmp(argv[i], "-depth"))
{
i++;
flags |= FTWJDEPTH;
}
else if (Istrcmp (argv[i], "-mount") ||
istrcmp (argv[i], "-xdev"))
{
i++;
flags |= FTW_MOUNT;
}
else if (Istrcmp (argv[i], "-mindepth") ||
Istrcmp (argv[i], "-maxdepth"))
{
i++;
if (i == argc)
problem =1;
else
{
tmp = strtoul (argv[i++], &chptr, 10);
if (*chptr)
problem = 1;
else if (Istrcmp(argv[i - 2], "-mindepth"))
minDepth = tmp;
else
maxDepth = tmp;
}
}
if (argc == 1 | | *argv[l] == '-')
{
argv[l] = ".";
argc = 2;
}
re = 0;
i = 1;
flags = 0;
while (i < argc && *argv[i] != f-f)
rc |= nftw(argv[i-f+], find, 100, flags);
return re;
}
20.
Устройства, предназначенные для интерактивного использования1, обладают
сходным интерфейсом, который был выведен десятилетия назад для последовательных
терминалов TeleType и получил название tty. Интерфейс tty используется для доступа к
последовательным терминалам, консолям, терминалам xterm, сетевым регистрационным
именам и тому подобному.
Интерфейс tty прост концептуально и называется termios .
Например, при сетевом подключении один конец устройства tty соединяется с
программой, предоставляющей сетевое подключение, а второй — с оболочкой, редактором
или другой потенциально интерактивной программой. Если программы находятся на
каждом конце, вы должны ясно понимать, на каком конце эмулируется оборудование;
при сетевом подключении к сети подключается аппаратная сторона.
Устройства tty с программным обеспечением на обоих концах называются
псевдотерминалами (pseudo-tty, или же просто pty). В первой части главы они рассматриваться
не будут, поскольку программный конец pty обрабатывается так же, как и любое другое
устройство tty. Позже мы поговорим о программировании аппаратного конца pty.
Интерфейсы tty работают в двух основных режимах: неформатируемый режим и
режим обработки. Неформатируемый режим передает данные в приложение без изменений.
Режим обработки, известный также как канонический режим, поддерживает
ограниченный построчный редактор внутри драйвера устройства и пересылает отредактированные
входные данные в приложение по одной строке за раз. Этот режим изначально
произрастает из универсальных систем, в которых специализированные устройства обработки
входных данных предоставляли режим обработки без прерывания ЦП.
Режим обработки обрабатывает определенные управляющие символы; например, по
умолчанию AU уничтожает (стирает) текущую строку, AW стирает текущее слово, забой
(АН) или Delete — предыдущий символ, a AR стирает и затем повторно набирает
текущую строку. Каждое из этих управляющих действий может быть повторно назначено
другом символу. Например, на многих терминалах символу DEL (код 127) назначается
действие забоя.
Каждый сеанс (см. главу 10) рривязан к терминалу, с которого процессы сеанса
получают свои входные данные и в который пересылают свои выходные данные. Этот
терминал может быть локальной консолью машины, терминалом, подключенным через
последовательный канал, либо псевдотерминалом, устанавливающим соответствия во
внешнем окне или по всей сети (подробнее о псевдотерминалах читайте в конце этой
главы). Терминал, к которому относится сеанс, называется управляющим терминалом
(или управляющим tty) сеанса. Терминал может быть управляющим терминалом только
в одном сеансе за раз.
Существуют две системные базы данных, используемые для отслеживания
зарегистрированных пользователей; utmp применяется для пользователей, зарегистрированных
в данный момент, a wtmp является записью всех предыдущих регистрации со времени
создания файла. Команда who использует базу данных utmp для отображения списка
зарегистрированных пользователей, а команда last — базу данных wtmp для
отображения списка пользователей, зарегистрированных в системе после регенерации базы
данных wtmp. В системах Linux база данных utmp хранится в файле /var/run/utmp, а база
данных wtmp — в файле /var/log/wtmp.