Лаборатория Tarantool

Системное программирование

Лекция 9:

Advanced IO. Неблокирующие IO операции. Блокировка файла. Мультиплексирование: select, poll, kqueue.

The presentation is outdated and not maintained anymore. Please, refer to the English version for the most actual slides.

Новости

Дедлайны:

  • планировщик корутин: 19 марта, штраф -10
  • shell: 2 апреля, штраф -10
  • файловая система: 16 апреля, штраф -3
  • пул потоков: 30 апреля

IPC [1]

Анонимные

int
pipe2(int pipefd[2], int flags);

void *
mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

int
mkfifo(const char *pathname, mode_t mode);

Именованные, стандарт XSI

int
semget(key_t key, int nsems, int semflg);

int
msgget(key_t key, int msgflg);

int 
shmget(key_t key, size_t size, int shmflg);

Именованные, стандарт POSIX

sem_t *
sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

IPC [2]. Сокеты

int
socket(int domain, int type, int protocol);

int
bind(int sockfd, const struct sockaddr *addr,
     socklen_t addrlen);

int
listen(int sockfd, int backlog);

int
connect(int sockfd, const struct sockaddr *addr,
        socklen_t addrlen);

int
accept(int sockfd, struct sockaddr *addr,
       socklen_t *addrlen);

IPC [3]. Сокеты

Способы использования

int fd = socket();
connect(fd, remote_addr);
/* Ready to read/write fd. */

Подключение к уже именованному

Создание именованного без соединения

int fd = socket();
bind(fd, addr);
/** Ready to read/write fd. */

Создание именованного с соединением

int fd = socket();
bind(fd, addr);
listen(fd);
while(1) {
        int remote_fd = accept(fd);
        /*
         * Ready to read/write
         * remote_fd.
         */
}

Connect() создает на сервере парный сокет

int fd2 = socket();
bind(fd2, addr2);
/** Ready to read/write fd2. */

read/write

send/recv

sendto/recvfrom

Без connect() работают только пакетные типы сокетов, и нужно указывать адресатов руками

IPC [4]. Сокеты

Server

Client

Client

Client

Client

New client

Как работать со многими клиентами?

int new_client_fd = accept(server_fd);
if (fork() == 0) {
        /* Read/write this client. */
        ...
        return 0;
}
/*
 * Server continues accepting new
 * clients.
 */

Выделять каждому процесс?

Выделять каждому поток?

int new_client_fd = accept(server_fd);
pthread_t tid;
/*
 * Put new client into a separate
 * thread.
 */
pthread_create(&tid, NULL,
               work_with_client_f,
               new_client_fd);
/*
 * Server continues accepting new
 * clients.
 */

IPC [5]. Сокеты

Почему нельзя просто брать и читать из каждого клиента как в коде ниже?

Accept(), read(), write() заблокируют поток до появления нового клиента/новых данных. Остальные будут простаивать.

int *client_fds = NULL;
int client_count = 0;
while (1) {
        int new_cli_fd = accept(server_fd);
        add_new_client(&client_fds, &client_count, new_cli_fd);
        for (int i = 0; i < client_count; ++i)
                interact_with_client(client_fds[i]);
}

2 балла

План лекции

  1. Неблокирующие IO операции
  2. Блокировки файлов
  3. Мультиплексирование дескрипторов
  4. Асинхронные IO операции
  5. Батчинг IO операций

Неблокирующие IO операции [1]

O_NONBLOCK

int fd = open(file_name, flags | O_NONBLOCK);
int old_flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
ssize_t rc = read/write/accept/send/recv(fd, ...);
if (rc == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
        /* No data to read, or space to write. */

Как установить флаг?

Как использовать?

или

Хороший код проверяет обе ошибки

Неблокирующие IO операции [2]

void
make_fd_nonblocking(int fd)
{
	int old_flags = fcntl(fd, F_GETFL);
	fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
}

int
main(int argc, const char **argv)
{
	make_fd_nonblocking(STDIN_FILENO);
	int value = 0;
	int rc = scanf("%d", &value);
	printf("scanf rc = %d\n", rc);
	printf("scanf error = %s\n", strerror(errno));
	rc = read(STDIN_FILENO, &value, sizeof(value));
	printf("read rc = %d\n", rc);
	printf("read error = %s\n", strerror(errno));

	while (1) {
		rc = scanf("%d", &value);
		if (rc > 0 || (errno != EAGAIN &&
                               errno != EWOULDBLOCK))
			break;
	}
	printf("value = %d\n", value);
	return 0;
}

Функция делает любой дескриптор неблокирующим

Даже поток ввода можно сделать неблокирующим

Тогда scanf и read будут возвращать ошибку, пока нет ввода

Буду читать в цикле, пока не прочитается

$> gcc 1_nonblock.c

$> ./a.out
scanf rc = -1
scanf error = Resource
    temporarily unavailable
read rc = -1
read error = Resource
    temporarily unavailable

            ...
100
value = 100

Блокировки файлов [1]

#define   LOCK_SH   1    /* Shared lock. */
#define   LOCK_EX   2    /* Exclusive lock. */
#define   LOCK_NB   4    /* Don't block when locking. */
#define   LOCK_UN   8    /* Unlock. */

int
flock(int fd, int operation);
/* Shared lock. */
flock(fd, LOCK_SH);

/* Exclusive lock. */
flock(fd, LOCK_EX);

/* Do not block on flock(). */
flock(fd, LOCK_SH | LOCK_NB);
flock(fd, LOCK_EX | LOCK_NB);

/* Unlock. */
flock(fd, LOCK_UN);

Блокировка файла целиком

Блокировки файлов [2]

int main()
{
	int fd = open("tmp.txt", O_CREAT | O_RDWR, S_IRWXU);
	char cmd[100];
	while (scanf("%s", cmd) > 0) {
		int rc;
		if (strcmp(cmd, "excl") == 0) {
			rc = flock(fd, LOCK_EX);
		} else if (strcmp(cmd, "exclnb") == 0) {
			rc = flock(fd, LOCK_EX | LOCK_NB);
		} else if (strcmp(cmd, "shared") == 0) {
			rc = flock(fd, LOCK_SH);
		} else if (strcmp(cmd, "sharednb") == 0) {
			rc = flock(fd, LOCK_SH | LOCK_NB);
		} else if (strcmp(cmd, "unlock") == 0) {
			rc = flock(fd, LOCK_UN);
		} else if (strcmp(cmd, "write") == 0) {
			rc = write(fd, "data", 4);
		} else {
			printf("unknown command\n");
			continue;
		}
		if (rc == -1)
			printf("error = %s\n", strerror(errno));
		else
			printf("ok\n");
	}
	close(fd);
	return 0;
}

Открываю/создаю файл

Можно выбрать любой лок, или снять его

Блокировки файлов

$> gcc 2_flock.c

$> ./a.out
$> ./a.out
excl
ok
excl
...
unlock
ok
ok
unlock
ok
shared
ok
shared
ok
exclnb
error = Resource temporarily unavailable
unlock
ok
exclnb
ok
sharednb
error = Resource temporarily unavailable
^D

$>
sharednb
ok
$> ./a.out
write
ok

Блокировки файлов [3]

int
flock(int fd, int operation);

Недостатки:

  • блокировка только целиком
  • advisory лок - то есть блокирует не IO операции, а только сам вызов flock()

Блокировка диапазонов в файле [1]

/* cmd = ... */
#define F_GETLK   /* Check if lock exists. */
#define F_SETLK   /* Do nonblocking lock. */
#define F_SETLKW  /* Do blocking lock. */

int
fcntl(int fd, int cmd, struct flock *lock_def);
struct flock {
        off_t       l_start;
        off_t       l_len;
        pid_t       l_pid;
        short       l_type;
        short       l_whence;
};
l_start, l_len

- начало и длина диапазона

l_whence

- откуда отсчитывать начало диапазона? С начала, конца, текущей позиции fd?

l_type

- взять лок на чтение (shared), на запись (exclusive), снять лок с диапазона

l_pid

- PID процесса, который держит лок, если такой есть

Блокировка диапазонов в файле [2]

Файл

0

100

lock.l_start = 60;
lock.l_whence = SEEK_SET;
lock.l_start = 20;
lock.l_whence = SEEK_CUR;
lock.l_start = -40;
lock.l_whence = SEEK_END;
struct flock lock;
lock.l_len = 30;
lock.l_type = F_WRLCK;
fcntl(fd, F_SETLK, &lock);

start

0

100

fd

start

0

start

60

20

-40

100

40

Блокировка диапазонов в файле

Файл

100

struct flock lock;
lock.l_len = 30;
lock.l_type = F_WRLCK;
lock.l_start = 60;
lock.l_whence = SEEK_SET;
fcntl(fd, F_SETLK, &lock);
struct flock lock;
lock.l_len = 20;
lock.l_type = F_WRLCK;
lock.l_start = 10;
lock.l_whence = SEEK_SET;
fcntl(fd, F_SETLK, &lock);
struct flock lock;
lock.l_len = 20;
lock.l_type = F_WRLCK;
lock.l_start = 40;
lock.l_whence = SEEK_SET;
fcntl(fd, F_SETLK, &lock);
struct flock lock;
lock.l_len = 20;
lock.l_type = F_UNLCK;
lock.l_start = 50;
lock.l_whence = SEEK_SET;
fcntl(fd, F_SETLK, &lock);

60

90

10

30

40

50

70

Блокировка диапазонов в файле [3]

int char_to_whence(char whence)
{
	if (whence == 's')
		return SEEK_SET;
	return whence == 'e' ? SEEK_END : SEEK_CUR;
}

int char_to_type(char type)
{
	if (type == 'r')
		return F_RDLCK;
	return type == 'w' ? F_WRLCK : F_UNLCK;
}

int do_lock(int fd, bool block, char *cmd)
{
	char whence, type;
	int start, len;
	sscanf(cmd, "%c %c %d %d", &type, &whence, &start, &len);
	printf("type = %c, whence = %c, start = %d, len = %d\n", type, whence,
	       start, len);
	struct flock fl;
	fl.l_type = char_to_type(type);
	fl.l_whence = char_to_whence(whence);
	fl.l_start = start;
	fl.l_len = len;
	return fcntl(fd, block ? F_SETLKW : F_SETLK, &fl);
}

Пример принимает строки вида:

<cmd> <type> <whence> <start> <len>

whence - s (set), e (end), c (cur)

type - r (read lock), w (write lock), u (unlock)

cmd - lock, lockb, unlock, getlock

Реализация команд установки/снятия лока

int
get_lock(int fd, char *cmd)
{
	char whence, type;
	int start, len;
	sscanf(cmd, "%c %c %d %d", &type, &whence, &start, &len);
	printf("type = %c, whence = %c, start = %d, len = %d\n", type, whence,
	       start, len);
	struct flock fl;
	fl.l_type = char_to_type(type);
	fl.l_whence = char_to_whence(whence);
	fl.l_start = start;
	fl.l_len = len;
	if (fcntl(fd, F_GETLK, &fl) == -1)
		return -1;
	if (fl.l_type == F_UNLCK)
		printf("no lock on this region\n");
	else
		printf("process %d holds the lock\n", (int)fl.l_pid);
	return 0;
}

Проверка, есть ли на диапазоне лок

Выбираю, какой диапазон проверить

Использую fcntl с командой F_GETLK - она в fl.l_pid положит pid процесса, держащего лок

Если диапазон пока свободен - fl.l_type будет изменен ядром на F_UNLCK

Иначе в fl.l_pid лежит валидный pid

int main()
{
	printf("my pid = %d\n", (int) getpid());
	int fd = open("tmp.txt", O_CREAT | O_RDWR, S_IRWXU);
	char *buf = NULL;
	size_t size = 0;
	while (getline(&buf, &size, stdin) > 0) {
		char *line = buf;
		line[strlen(line) - 1] = 0;
		int rc;
		char *cmd = strsep(&line, " \n");
		if (strcmp(cmd, "write") == 0) {
			rc = write_symbols(fd, line);
		} else if (strcmp(cmd, "lock") == 0) {
			rc = do_lock(fd, false, line);
		} else if (strcmp(cmd, "lockb") == 0) {
			rc = do_lock(fd, true, line);
		} else if (strcmp(cmd, "getlock") == 0) {
			rc = get_lock(fd, line);
		}
		if (rc == -1)
			printf("error = %s\n", strerror(errno));
		else
			printf("ok\n");
	}
	close(fd);
	return 0;
}

Сама командная строка

Простая реализация write, чтобы чем-то заполнить файл

Локи/анлоки

Блокировка диапазонов в файле [4]

$> gcc 2_flock.c

$> ./a.out
my pid = 95231
$> ./a.out
my pid = 95229
write 100 a
ok

0

100

lock w(rite) s(et) 10 20
ok
lock r(ead) s 40 20
ok

- write lock

- read lock

lock r s 50 30
ok
lock w s 20 30
error = Resource temporarily unavailable
lockb w s 20 30
...
lock u(nlock) s 20 10
ok
lock u(nlock) s 40 10
ok
ok
lock w s 100 0
ok
write 10 a
ok

110

lock w s 101 9
error = Resource temporarily unavailable
^C

$>
lock w s 100 10
ok

10

20

30

40

50

60

80

Блокировка диапазонов в файле [5]

#define F_ULOCK    /* Unlock locked sections. */
#define F_LOCK     /* Lock a section for exclusive use. */
#define F_TLOCK    /* Test and lock a section for exclusive use. */
#define F_TEST     /* Test a section for locks by other processes. */
int
lockf(int fildes, int function, off_t size);
struct flock fl;
fl.l_start = 0;
fl.l_len = size;
fl.l_whence = SEEK_CUR;
if (function == F_TEST) {
        fl.l_type = F_WRLCK;
        if (fcntl(fd, F_GETLK, &fl) != 0)
            return -1;
        if (fl.l_type == F_UNLCK)
            return 0;
        errno = EAGAIN;
        return -1;
}
if (function == F_ULOCK) {
        fl.l_type = F_UNLCK;
        return fcntl(fd, F_SETLK, &fl);
}
fl.l_type = F_WRLCK;
return fcntl(fd, function == F_LOCK ? F_SETLK : F_SETLKW, &fl);

Возможная реализация:

Блокировка файлов [1]

int
lockf(int fildes, int function, off_t size);

int
fcntl(int fd, int cmd, struct flock *lock_def);

int
flock(int fd, int operation);

Взаимосвязаны

Не наследуются при fork()

Принадлежат процессу, не потоку. В одном процессе можно брать лок много раз

Они все advisory

Блокировка файлов [2]

Как можно использовать эти локи в виде мьютекса?

1) использовать целый файл, как мьютекс, 2) использовать любой один диапазон файла (первый байт, например)

int
lockf(int fildes, int function, off_t size);

int
fcntl(int fd, int cmd, struct flock *lock_def);

int
flock(int fd, int operation);

1 балл

Мультиплексирование дескрипторов [1]

Server

Client

Client

Client

Client

Как понять, из какого сокета можно читать?

События, данные

void make_fd_nonblocking(int fd)
{
	int old_flags = fcntl(fd, F_GETFL);
	fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
}

/* ... */

/* Make 'accept(server_fd)' non-blocking. */
make_fd_nonblocking(server_fd)
int *client_fds = NULL;
int client_count = 0;
while (1) {
        int new_cli_fd = accept(server_fd);
        if (new_cli_fd != -1) {
                /* Make read/write(new_cli_fd) non-blocking. */
                make_fd_nonblocking(new_cli_fd);
                add_new_client(&client_fds, &client_count, new_cli_fd);
        }
        for (int i = 0; i < client_count; ++i)
                interact_with_client(client_fds[i]);
}

Опрашивать всех по очереди? - polling

Лишние системные вызовы - трата CPU

Задержка обработки линейна от числа дескрипторов

Сделать все дескрипторы неблокирующими

Обходить по кругу и проверять EAGAIN, EWOULDBLOCK

Мультиплексирование дескрипторов [2]

  • select()
  • poll()
  • kqueue (Mac, BSD)
  • epoll (Linux)

Сокеты

Файлы

Терминалы

Фьютексы

Мультиплексирование дескрипторов [3]

Мультиплексирование. Select [1]

void FD_CLR(fd, fd_set *fdset);

void FD_COPY(fd_set *fdset_orig, fd_set *fdset_copy);

int FD_ISSET(fd, fd_set *fdset);

void FD_SET(fd, fd_set *fdset);

void FD_ZERO(fd_set *fdset);

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *errorfds, struct timeval *timeout);

Три набора файловых дескрипторов: для отслеживания возможности читать, писать, исключений

Функции для создания и работы с одним fd_set - набором дескрипторов

Значение максимального файлового дескриптора во всех трех наборах

Select() блокируется до возникновения события на одном из дескрипторов

На событие вернется, стерев из fd_set тех, у кого событий нет

fd_set set;
FD_ZERO(&set);
FD_SET(file_desc1, &set);
FD_SET(file_desc2, &set);
select(MAX(file_desc1, file_desc2) + 1, &set,
       NULL, NULL, NULL);
if (FD_ISSET(file_desc1, &set)) {
        /* file_desc1 has data to read. */
        read(file_desc1, buffer, buf_size);
        /* ... */
}
if (FD_ISSET(file_desc2, &set)) {
        /* ... */
}
  • После каждого срабатывания надо пересобирать все fd_set
  • Может поменять таймаут
  • Размер fd_set ограничен (обычно FD_SETSIZE = 1024)

Мультиплексирование. Select [2]

int
main(int argc, const char **argv)
{
	int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(12345);
	inet_aton("127.0.0.1", &addr.sin_addr);
	connect(sock, (struct sockaddr *) &addr, sizeof(addr));
	int number;
	while (scanf("%d", &number) > 0) {
		if (write(sock, &number, sizeof(number)) == -1) {
			printf("error = %s\n", strerror(errno));
			continue;
		}
		printf("Sent %d\n", number);
		number = 0;
		int rc = read(sock, &number, sizeof(number));
		if (rc == 0) {
			printf("Closed connection\n");
			break;
		}
		if (rc == -1)
			printf("error = %s\n", strerror(errno));
		else
			printf("Received %d\n", number);
	}
	close(sock);
	return 0;
}

Обычный клиент. Посылает числа, ему отвечают увеличенными на 1

Мультиплексирование. Select [3]

int fill_fdset(fd_set *set, int *clients, int client_count, int server)
{
	int max_fd = server;
	FD_ZERO(set);
	FD_SET(server, set);
	for (int i = 0; i < client_count; ++i) {
		FD_SET(clients[i], set);
		if (clients[i] > max_fd)
			max_fd = clients[i];
	}
	return max_fd;
}

int main(int argc, const char **argv)
{
	int server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(12345);
	inet_aton("127.0.0.1", &addr.sin_addr);

	bind(server, (struct sockaddr *) &addr, sizeof(addr));
	listen(server, 128);
	int client_count = 0, *clients = NULL;
	fd_set readset;
	while(1) {
		int max_fd = fill_fdset(&readset, clients, client_count, server);
		struct timeval timeval;
		timeval.tv_sec = 2;
		timeval.tv_usec = 0;

Создание сервера, начало прослушки порта

Заполнение readset перед каждым select() заново

                int nfds = select(max_fd + 1, &readset, NULL, NULL, &timeval);
		if (nfds == 0) {
			printf("Timeout\n");
			continue;
		}
		if (FD_ISSET(server, &readset)) {
			int client_sock = accept(server, NULL, NULL);
			printf("New client\n");
			client_count++;
			clients = realloc(clients, client_count * sizeof(int));
			clients[client_count - 1] = client_sock;
			nfds--;
		}
		for (int i = 0; i < client_count && nfds > 0; ++i) {
			if (! FD_ISSET(clients[i], &readset))
				continue;
			nfds--;
			printf("Interact with fd %d\n", clients[i]);
			int rc = interact(clients[i]);
			if (rc == 0) {
				printf("Client disconnected\n");
				remove_client(&clients, &client_count, i);
			}
		}
	}
	close(server);
	for (int i = 0; i < client_count; ++i)
		close(clients[i]);
	free(clients);
	return 0;
}

Select() вернет, сколько дескрипторов имеют события. 0 - таймаут

Сначала проверю server - он особенный. На серверных сокетах новый клиент срабатывает как read событие

Каждого клиента проверить

Результат select - подсказка, которая позволит не сканировать каждый раз все дескрипторы на FD_ISSET

Мультиплексирование. Select [4]

$> gcc 4_server_select.c -o server

$> ./server
$> gcc 4_client.c -o client

$> ./client
$> ./client
Timeout
Timeout
New client
New client
100
Sent 100
Received 101
Interact with fd 4
Received 100
Sent 101
200
Sent 200
Received 201
Interact with fd 5
Received 200
Sent 201
^C

$>
Interact with fd 5
Client disconnected
^C

$>
Interact with fd 4
Client disconnected

Мультиплексирование. Poll [1]

int
poll(struct pollfd fds[], nfds_t nfds, int timeout);

struct pollfd {
        int fd; /* File descriptor. */
        short events; /* Events to look for. */
        short revents; /* Events returned. */
};

#define POLLERR /* Wait for exceptions. */
#define POLLIN /* Wait for ability to read. */
#define POLLOUT /* Wait for ability to write. */

/* POLLHUP, POLLNVAL, POLLPRI, POLLRDBAND, POLLRDNORM,
POLLWRBAND, POLLWRNORM */
struct pollfd fds[2];
fds[0].fd = file_desc1;
fds[0].events = POLLIN;
fds[1].fd = file_desc2;
fds[1].events = POLLIN | POLLOUT;
poll(fds, 2, 2000);
if (fds[0].revents | POLLIN)
        /* Can safely read from file_desc1. */

if (fds[1].revents | POLLIN)
        /* Can safely read from file_desc2. */

if (fds[1].revents | POLLOUT)
        /* Can safely write to file_desc2. */ 

В каждом pollfd задается дескриптор, и что в нем хочется слушать

После возврата poll положит сюда реально произошедшие события

  • Не ограничено число дескрипторов
  • Не надо пересобирать массив pollfd после poll()
  • Точность таймаута - лишь миллисекунды
  • Ядро будет проходить весь массив на каждый poll()

Мультиплексирование. Poll [2]

	struct pollfd *fds = malloc(sizeof(fds[0]));
	int fd_count = 1;
	fds[0].fd = server;
	fds[0].events = POLLIN;
	while(1) {
		int nfds = poll(fds, fd_count, 2000);
		if (nfds == 0) {
			printf("Timeout\n");
			continue;
		}
		if ((fds[0].revents & POLLIN) != 0) {
			int client_sock = accept(server, NULL, NULL);
			printf("New client\n");
			fd_count++;
			fds = realloc(fds, fd_count * sizeof(fds[0]));
			fds[fd_count - 1].fd = client_sock;
			fds[fd_count - 1].events = POLLIN;
			nfds--;
		}
		for (int i = 0; i < fd_count && nfds > 0; ++i) {
			if ((fds[i].revents & POLLIN) == 0)
				continue;
			nfds--;
			printf("Interact with fd %d\n", fds[i].fd);
			if (interact(fds[i].fd) == 0) {
				printf("Client disconnected\n");
				remove_client(&fds, &fd_count, i);
			}
		}
	}

Вместо массива int теперь массив pollfd

Poll, как и select, вернет, сколько pollfd имеют события

Добавление, как и раньше, через добавку в массив pollfd

У клиентов проверяем, у кого есть событие чтения

Мультиплексирование. Kqueue [1]

int
kqueue(void);

int
kevent(int kq,
       const struct kevent *changelist, int nchanges,
       struct kevent *eventlist, int nevents,
       const struct timespec *timeout);

struct kevent {
        uintptr_t ident; /* Identifier for this event. */
        int16_t filter; /* Filter for event. */
        uint16_t flags; /* General flags. */
        uint32_t fflags; /* Filter-specific flags. */
        intptr_t data; /* Filter-specific data. */
        void *udata; /* Opaque user data identifier. */
};

EV_SET(&kev, ident, filter, flags, fflags, data, udata);

Kernel Events Queue

Создает очередь и возвращает ее "файловый" дескриптор

Управление очередью и извлечение событий

Список изменений вида "отслеживать новое событие", "перестать отслеживать другое"

Сюда будут положены случившиеся события

kevent может следить за многими системными событиями. В случае IO тут файловый дескриптор

Что отслеживать? Чтение, запись, исключения, закрытие ... ?

Действие - удалить, добавить, поменять события

Сюда можно положить любые данные, и они будут без изменений появляться вместе с этим событием

int kq = kqueue();

Создание очереди

struct kevent new_ev;
EV_SET(&new_ev, fd, EVFILT_READ/WRITE/..., EV_ADD, 0, 0, 0);
kevent(kq, &new_ev, 1, 0, 0, NULL);

Отслеживание нового события на дескрипторе fd. На каждое событие (read, write) нужен свой kevent

Удаление события из отслеживания

struct kevent old_ev;
EV_SET(&old_ev, fd, EVFILT_READ/WRITE/..., EV_DELETE, 0, 0, 0);
kevent(kq, &old_ev, 1, 0, 0, NULL);

Получение случившихся событий

struct kevent happened_ev;
kevent(kq, NULL, 0, &happened_ev, 1, NULL);
if (happened_ev.filter == EVFILT_READ)
        /* Can safely read from happened_ev.ident. */
if (happened_ev.filter == EVFILT_WRITE)
        /* Can safely write to happened_ev.ident. */

Мультиплексирование. Kqueue [2]

	int kq = kqueue();
	struct kevent new_ev;
	EV_SET(&new_ev, server, EVFILT_READ, EV_ADD, 0, 0, 0);
        kevent(kq, &new_ev, 1, 0, 0, NULL);
	struct timespec timeout;
	timeout.tv_sec = 2;
	timeout.tv_nsec = 0;
	while(1) {
		if (kevent(kq, NULL, 0, &new_ev, 1, &timeout) == 0) {
			printf("Timeout\n");
			continue;
		}
		if (new_ev.ident == server) {
			int client_sock = accept(server, NULL, NULL);
			printf("New client\n");
			EV_SET(&new_ev, client_sock, EVFILT_READ, EV_ADD, 0, 0, 0);
			kevent(kq, &new_ev, 1, 0, 0, NULL);
		} else {
			printf("Interact with fd %d\n", (int)new_ev.ident);
			interact(new_ev.ident);
			if ((new_ev.flags & EV_EOF) != 0) {
				printf("Client disconnected\n");
				close(new_ev.ident);
			}
		}
	}
	close(kq);

Массива больше нет, теперь очередь в ядре

Добавляю на прослушку "чтения" с серверного сокета

События буду получать по одному. Можно было передать массив, но можно и одно событие

Так как возвращается не массив, по индексу больше не понятно, какое событие к кому относится. Здесь использую сравнение с new_ev.ident

Регистрирую прослушку чтений с нового клиента

В отличие от select, poll, на закрытие дескриптора явно выдается флаг EV_EOF

Очередь уничтожается обычным close()

Мультиплексирование. Kqueue [3]

  • Не ограничено число дескрипторов
  • Нет массива в user space, который надо каждый раз передавать в ядро
  • Большой выбор событий, не только на IO
  • Точно можно выбрать вплоть до наносекунд
  • Ужасный интерфейс
  • Только на Mac, BSD

Мультиплексирование. Epoll [1]

int
epoll_create(int size);

int
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int
epoll_wait(int epfd, struct epoll_event *events,
           int maxevents, int timeout);

typedef union epoll_data {
        void *ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
} epoll_data_t;

struct epoll_event {
        uint32_t events; /* Epoll events. */
        epoll_data_t data; /* User data variable. */
};

Создание очереди событий

Управление очередью: добавление дескрипторов, удаление, изменение

Ожидание событий

С каждым файловым дескриптором ассоциируется набор нужных от него событий

И произвольные пользовательские данные, как в kevent

int ep = epoll_create(12345);

Создание очереди. Ее размер сейчас игнорируется, а в API остался с давних времен, когда было не так

struct epoll_event new_ev;
new_ev.events = EPOLLIN | EPOLLOUT;
new_ev.data.fd/u32/u64/ptr = my_any_data;
epoll_ctl(ep, EPOLL_CTL_ADD, file_desc, &new_ev);

Добавление дескриптора на отслеживание событий

Удаление дескриптора из очереди

epoll_ctl(ep, EPOLL_CTL_DEL, file_desc, NULL);

Мультиплексирование. Epoll [2]

	int ep = epoll_create(1);
	struct epoll_event new_ev;
	new_ev.data.fd = server;
	new_ev.events = EPOLLIN;
	epoll_ctl(ep, EPOLL_CTL_ADD, server, &new_ev);
	while(1) {
		if (epoll_wait(ep, &new_ev, 1, 2000) == 0) {
			printf("Timeout\n");
			continue;
		}
		if (new_ev.data.fd == server) {
			int client_sock = accept(server, NULL, NULL);
			printf("New client\n");
			new_ev.data.fd = client_sock;
			new_ev.events = EPOLLIN;
			epoll_ctl(ep, EPOLL_CTL_ADD, client_sock, &new_ev);
		} else {
			printf("Interact with fd %d\n", (int)new_ev.data.fd);
			if (interact(new_ev.data.fd) == 0) {
				printf("Client disconnected\n");
				close(new_ev.data.fd);
				epoll_ctl(ep, EPOLL_CTL_DEL, new_ev.data.fd,
					  NULL);
			}
		}
	}
	close(ep);

Создается очередь, по аналогии с kqueue

Регистрируем сервер с ожиданием событий "чтения". Пользовательские данные - сам сокет

Ждем события

Использую пользовательские данные, куда я положил дескриптор, чтобы определить источник события

Каждый новый клиент тоже регистрируется

Закрытые дескрипторы приходится самому удалять через epoll_ctl

Уничтожается тоже через close()

Мультиплексирование. Epoll [3]

  • Не ограничено число дескрипторов
  • Нет массива в user space, который надо каждый раз передавать в ядро
  • Только на Linux
  • Точность таймаута - миллисекунды

POSIX AIO [1]

int aio_read(struct aiocb *aiocbp);

int aio_write(struct aiocb *aiocbp);

int aio_error(const struct aiocb *aiocbp);

ssize_t aio_return(struct aiocb *aiocbp);

int aio_suspend(const struct aiocb *const list[],
                int nent, const struct timespec *timeout);

int aio_cancel(int fildes, struct aiocb *aiocbp);

int lio_listio(int mode, struct aiocb *const aiocb_list[],
               int nitems, struct sigevent *sevp);

struct aiocb {
       int aio_fildes; /* File descriptor. */
       off_t aio_offset; /* File offset. */
       volatile void  *aio_buf; /* Location of buffer. */
       size_t aio_nbytes; /* Length of transfer. */
       int aio_reqprio; /* Request priority. */
       struct sigevent aio_sigevent; /* Notification method. */
       int aio_lio_opcode; /* lio_listio specific. */
};

POSIX AIO [2]

char buffer[1024];

struct aiocb cb;
memset(&cb, 0, sizeof(cb));
cb.aio_fieldes = fd;
cb.aio_offset = lseek(fd, 0, SEEK_CUR);
cb.aio_buf = buffer;
cb.aio_nbytes = sizeof(buffer);
aio_read(&cb);

/* Do some non-related work ... */

while (aio_error(&cb) == EINPROGRESS) {};

int result = aio_return(&cb);
char buffer[1024];

struct aiocb cb;
memset(&cb, 0, sizeof(cb));
cb.aio_fieldes = fd;
cb.aio_offset = lseek(fd, 0, SEEK_CUR);
cb.aio_buf = buffer;
cb.aio_nbytes = sizeof(buffer);
aio_read(&cb);

/* Do some non-related work ... */

aio_suspend(&cb, 1, NULL);

int result = aio_return(&cb);

=

char buffer[1024];
read(fd, buffer, sizeof(buffer));

POSIX AIO

const int chunk_size = 1024;
char buffer[chunk_size * 3];

struct aiocb cb[3];
memset(cb, 0, sizeof(cb));
for (int i = 0; i < 3; ++i) {
        cb[i].aio_fieldes = fd;
        cb[i].aio_nbytes = chunk_size;
        cb[i].aio_lio_opcode = LIO_READ;
}

cb[1].aio_offset = lseek(fd, 0, SEEK_CUR);
cb[2].aio_offset =
        cb[1].aio_offset + chunk_size;
cb[3].aio_offset =
        cb[2].aio_offset + chunk_size;

cb[1].aio_buf = buffer;
cb[2].aio_buf = buffer + chunk_size;
cb[3].aio_buf = buffer + chunk_size * 2;

lio_listio(LIO_NOWAIT, cb, 3, NULL);

/* Do some non-related work ... */

aio_suspend(cb, 3, NULL);

int result1 = aio_return(&cb[1]);
int result2 = aio_return(&cb[2]);
int result3 = aio_return(&cb[3]);
const int chunk_size = 1024;
char buffer[chunk_size * 3];

read(fd, buffer, chunk_size);
read(fd, buffer + chunk_size,
     chunk_size);
read(fd, buffer + 2 * chunk_size,
     chunk_size);

=

POSIX AIO [3]

Почему не надо использовать?

  • Не очень популярное API, могут выкинуть
  • Медленно - это не системные вызовы, а мини-библиотека внутри libc на pthread
  • Уведомления о завершении могут создавать/удалять потоки

POSIX AIO

Что использовать вместо?

  • Синхронные операции
  • Создать свой собственный IO-worker поток

Батчинг IO операций

ssize_t
writev(int fildes, const struct iovec *iov, int iovcnt);

ssize_t
readv(int d, const struct iovec *iov, int iovcnt);

struct iovec {
        char *iov_base;
        size_t iov_len;
};
char buffer[2][512];
struct iovec vec[2];

vec[0].iov_base = buffer[0];
vec[0].iov_len = sizeof(buffer[0]);
vec[1].iov_base = buffer[1];
vec[1].iov_len = sizeof(buffer[1]);

writev(fd, vec, 2);

=

char buffer[2][512];

write(fd, buffer[0], sizeof(buffer[0]));
write(fd, buffer[1], sizeof(buffer[1]));

Экономия числа системных вызовов, и не нужно копировать все в один буфер

Заключение

В следующий раз:

Пользователи и группы. Вход в систему. Real and effective user. Права доступа у процессов, файлов. Сессии. Демонизация процесса.

Системное программирование 9

By Vladislav Shpilevoy

Системное программирование 9

Advanced IO. Неблокирующие IO операции. Блокировка файла: flock, lockf, fcntl. Multiplexed IO: select, poll, kqueue. Async IO: aio_read/write.

  • 1,636