CC BY-SA-NC Mikael Voss

POSIX Sockets

POSIX Sockets

CC BY-SA-NC Mikael Voss

Overview

A network socket is an endpoint of a bidirectional communication flow.

The POSIX sockets API, which is based on the Berkeley sockets API introduced to 4.2BSD in 1983, is supported by a wide range of operating systems, including Linux, all flavours of BSD and other unixoid systems. Even the Windows family of operating systems implements a subset of this interface through its sockets API.
RFC 2553 specifies an extension to the socket API to support IPv6.

Socket creation

A socket can be created using the socket system call.1

#include <sys/socket.h>

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

Where domain specifies the communication domain, type the socket type and protocol the actual transport protocol. If successful, the function returns a non‐negative socket descriptor. Otherwise it returns -1and sets errno to indicate the error.

Communication domains

The system header file <sys/socket.h> usually defines the following macros as communication domains:

AF_UNIXLocal communicationAF_INETInternet protocol version 4 (IPv4)AF_INET6Internet protocol version 6 (IPv6)

The AF_UNIX socket family is used for communication between processes on the same machine, while AF_INET and AF_INET6 sockets are usually used for communication with different hosts.

Socket types

The socket type specifies the communication semantics. The <sys/socket.h> header defines at least the following macros:

SOCK_DGRAMUnreliable datagramsSOCK_STREAMReliable byte‐streamSOCK_SEQPACKETReliable sequenced packets

SOCK_DGRAM sockets transmit unreliable datagrams of a fixed maximum length, whereas SOCK_STREAM and SOCK_SEQPACKET sockets are connection‐oriented and reliable. While SOCK_STREAM sockets provide a sequenced byte‐stream and unlike SOCK_SEQPACKET sockets do not preserve message boundaries.
While SOCK_STREAM and SOCK_SEQPACKET sockets are reliable, their connection‐oriented nature does not allow them to be used for multicast communication (i.e. communication with more than one endpoint).

Protocols

If the protocol argument is specified as 0, the implementation‐dependent default protocol for the specified socket type is used. For SOCK_DGRAM sockets this will usually be the User Datagram Protocol (UDP) and for SOCK_STREAM sockets the Transmission Control Protocol (TCP).
If a specific protocol is desired, its number can be determined using the getprotobyname function.2

Example

The following example creates an IPv6 stream socket using the default protocol – usually TCP – and checks for possible errors. If the operation fails, socket will return -1, but as socket descriptors are always non‐negative, we shall check for any negative return value. The perror function declared in <stdio.h> prints the supplied message to the standard error stream followed by a description of the error indicated byerrno.
The EXIT_SUCCESS and EXIT_FAILURE macros from <stdlib.h> expand to the system‐dependent standard return codes for user programmes. On most systems the former will be defined as 0 and the latter as 1.

#include <stdio.h>
#include <stdlib.h>

#include <sys/socket.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

Binding

A socket created by the socket call is unnamed and needs to be bound to an address to be able to receive and transmit data. This can be done by a bind call or implicitly by using connect or sendto, in which case the socket will be bound to an unused local address.

#include <sys/socket.h>

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

The sockfd argument specifies the socket to bind, while address is a pointer to a domain‐dependent data structure of addrlen bytes. Upon success, the call returns 0 or -1 otherwise. In the latter case, it will seterrno to indicate the error.

IPv4 and IPv6

The address structure for AF_INET and AF_INET6 sockets is defined in the system header file <netinet/in.h>.

#include <sys/socket.h>
#include <netinet/in.h>

struct sockaddr_in {
sa_family_t sin_family; /* address family (AF_INET) */
in_port_t sin_port; /* port number */
struct in_addr sin_addr; /* address */
};

struct sockaddr_in6 {
sa_family_t sin6_family; /* address family (AF_INET6) */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* flow information */
struct in_addr6 sin6_addr; /* address */
uint32_t sin6_scope_id; /* scope ID */
}

The port number and address have to be given in network byte order (most significant byte first).

The struct sockaddr_in6 structure contains the fields sin6_flowinfo and sin6_scope_id that should be set to zero.

UNIX

UNIX sockets are bound to path names in the file system. Their address structure is defined in the system header file <sys/un.h>.
Using the socketpair call, one can create a pair of unnamed connected sockets that do not need to be bound.

#include <sys/socket.h>
#include <sys/un.h>

struct sockaddr_un {
sa_family_t sun_family; /* address family (AF_UNIX) */
char sun_path[UNIX_PATH_MAX]; /* path name */
};

Example

In the following example, we create an IPv6 stream socket and bind it to the address in6addr_any, which will bind the socket to all addresses of the local host. As the port number needs to be given in network byte order, we use the htons (host to network short) macro from <arpa/inet.h>.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
return EXIT_FAILURE;
}

struct sockaddr_in6 addr;
memset(&addr, 0, sizeof addr);
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(1024);
addr.sin6_addr = in6addr_any;

if (bind(sockfd, (struct sockaddr *) &addr, sizeof addr)) {
perror("Could not bind socket");
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

Connection‐oriented sockets

Connection‐oriented (i.e. SOCK_STREAM) sockets need to be connected to a network address using the connect call or be marked as a passive socket using listen. Afterwards, connections can be accepted with the accept call.

Connecting

#include <sys/socket.h>

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

Where sockfd is the socket descriptor to connect, addr a pointer to the address to connect to and addrlen the size of the address structure. On success, 0 is returned, while on failure, ‐1 is returned and errno set appropriately.
If the socket has not been bound, yet, it will be bound to an unused local address. Connection‐oriented protocol sockets may only be connected once!

Example

The following example will create a stream socket and connect it to the IPv6 address 2001:db8::1, Port 443 (HTTPS). To convert the IPv6 from its text form to its binary representation, we shall use theinet_pton function from <arpa/inet.h>.

#include <stdio.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
return EXIT_FAILURE;
}

struct sockaddr_in6 addr;
memset(&addr, 0, sizeof addr);
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(1024);

if (inet_pton(AF_INET6, "2001:db8::1", &addr.sin6_addr)) {
perror("Could not convert IPv6 address to binary form");
return EXIT_FAILURE;
}

if (connect(sockfd, (struct sockaddr *) &addr, sizeof addr)) {
perror("Failed to connect socket");
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

After successfully connecting the socket, we may begin to read from or write to the socket.

Listening

#include <sys/socket.h>

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

The listen call will mark the socket named by sockfd as passive and will set the size of the queue of pending (i.e. not yet accepted) connections to backlog. Upon success, it will return 0 or -1 otherwise. In the latter case, errno will be set to indicate the error.

The accept call dequeues the first connection request for the listening socket sockfd and creates a new connected socket, storing the remote address to addr and its length to addrlen, which should contain the storage capacity of the structure. If addr is specified as (struct sockaddr *) 0, nothing will be stored.
Upon success, it will return the newly created socket or -1 otherwise and set errno appropriately.

Example

In the following example, we create a stream socket, bind it to the any address and mark it as listening to accept incoming connections.
The accept call will block the process until a connection has been established, after which we shall print the address of the connecting host. To do this, we have to convert the address from its binary representation to text form using the inet_ntop function from <arpa/inet.h>. As the port number is in network byte order, we have to transform it to the native byte order of the local machine with the ntohsmacro.

#include <stdio.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
return EXIT_FAILURE;
}

struct sockaddr_in6 addr;
memset(&addr, 0, sizeof addr);
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(1024);
addr.sin6_addr = in6addr_any;

if (bind(sockfd, (struct sockaddr *) &addr, sizeof addr)) {
perror("Could not bind socket");
return EXIT_FAILURE;
}


if (listen(sockfd, 128)) {
perror("Unable to listen for connections");
return EXIT_FAILURE;
}

struct sockaddr_in6 conn;
socklen_t clen = sizeof conn;
int connfd = accept(sockfd, (struct sockaddr *) &conn, &clen);

if (connfd < 0) {
perror("Could not accept connection");
return EXIT_FAILURE;
}

char addrbuf[INET6_ADDRSTRLEN];
if (!inet_ntop(AF_INET6, &conn.sin6_addr, addrbuf, sizeof addrbuf)) {
perror("Unable to convert remote address to text form");
return EXIT_FAILURE;
}

printf("Incoming connection from %s, port %hu\n", addrbuf, ntohs(conn.sin6_port));
return EXIT_SUCCESS;
}

Reception and transmission

Data can be received and transmitted using the standard read and write calls.

#include <unistd.h>

ssize_t read(int sockfd, void *buf, size_t size);
ssize_t write(int sockfd, const void *buf, size_t size);

sockfd specifies the socket to read from or write to. In case of read, buf points to a buffer to hold the received data and size should contain its capacity, while in case of write, buf points to a buffer containing the data to be transmitted and size should contain their size.
Upon success, the calls return the number of bytes actually transmitted or received. On failure, -1 is returned and errno is set to indicate the error.
Please keep in mind, that both calls block the process until the socket is ready and may read or write less data than requested.

Example

The following example attempts to read 256 bytes from a connected stream socket and prints them.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
/* … */

ssize_t ret;
ssize_t off = 0;
char buf[256];
do {
ret = read(sockfd, buf + off, sizeof buf);
if (ret < 0) {
perror("Read error");
return EXIT_FAILURE;
}

off += ret;
} while (off < sizeof buf - 1);

for (size_t iter = 0; iter <= off; ++iter)
printf("%02x", buf[iter]);
putchar('\n');

return EXIT_SUCCESS;
}

Closing a connection

A connection can be closed like any other file descriptor using the close call.

#include <unistd.h>

int close(int sockfd);

Where sockfd refers to the socket descriptor. The call returns 0 on success or -1 on failure and sets errno accordingly.

If one needs to shut down one part of a full‐dulpex connection, the shutdown call can be used.

#include <sys/socket.h>

int shutdown(int sockfd, int how);

Where sockfd refers to the socket descriptor. The how argument may be one of the constants SHUT_RD, SHUT_WR or SHUT_RDWR to disallow further receptions, transmissions or both, respectively. Like most other calls, it returns 0 on succes or -1 on failure and sets errno to indicate the error.

Connection‐less sockets

SOCK_DGRAM sockets do not need to be connected or marked as listening to be able to transmit and receive data and may be used for multicast communication, but they are unreliable (unlike SOCK_STREAM orSOCK_SEQPACKET sockets, packets lost due to congestion will not be automatically resent), unordered (packets may be received in different order than they were transmitted) and packets cannot be larger than the path MTU for a specific destination. AF_INET6 sockets are guaranteed to have an MTU greater than or equal to 1280 octets.

Transmission

To transmit data on a SOCK_DGRAM socket, it may be connected to a specific destination address or one may specify the destination ad‐hoc using the sendto call. In both cases, it will be implicitly bound to an unused local address, if it has not been bound already. Unlike connection‐oriented sockets, datagram sockets may be connected more than once.

#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t size, int flags, const struct sockaddr *addr, socklen_t addrlen);

Where sockfd specifies the sending socket, buf the data to be transmitted and size its length. The flags argument will be discussed later and should be specified as 0 for now. addr is a pointer to the destination address structure and addrlen its size. When successful, the call will return the number of bytes transmitted or, on failure, it will return -1 and set errno accordingly. A returned size less than the supplied one indicates that the message was truncated.

Example

The following example will create an IPv6 datagram socket and transmit a packet containing the hexadecimal string deadbeef to 2001:db8::1, Port 1024.

#include <stdio.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
return EXIT_FAILURE;
}

struct sockaddr_in6 addr;
memset(&addr, 0, sizeof addr);
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(1024);

if (inet_pton(AF_INET6, "2001:db8::1", &addr.sin6_addr)) {
perror("Could not convert IPv6 address to binary form");
return EXIT_FAILURE;
}

const char buf[] = { 0xde, 0xea, 0xbe, 0xef };
ssize_t ret = sendto(sockfd, buf, sizeof buf, 0, &addr, sizeof addr);
if (ret < 0) {
perror("Transmission failure");
return EXIT_FAILURE;
}
else if (ret < sizeof addr) {
fputs("Message was truncated");
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

Reception

To receive data on a datagram socket, one can use read or recvfrom after binding it to a local address. Usually, all receive operations will read only a single packet and if a packet would be larger than the supplied buffer, the rest will be discarded.

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t size, int flags, struct sockaddr *addr, socklen_t *addrlen);

sockfd names the socket to receive from, buf specifies the buffer to store the packet to and size its capacity. As with sendto, the flags argument should be ignored for now. If addr is not (struct sockaddr *) 0, the source address will be stored to the memory location it points to and its size written to addrlen, which is used to supply the initial capacity. Upon success, it will return the number of bytes stored to bufor -1 on failure. In the latter case, errno is set to indicate the error.

Example

The following example will create an IPv6 datagram socket, bind it to the any address and wait for a datagram. Upon reception, it will print the source address and its content.

#include <stdio.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
return EXIT_FAILURE;
}

struct sockaddr_in6 addr;
memset(&addr, 0, sizeof addr);
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(1024);
addr.sin6_addr = in6addr_any;

if (bind(sockfd, (struct sockaddr *) &addr, sizeof addr)) {
perror("Could not bind socket");
return EXIT_FAILURE;
}

char buf[256];
struct sockaddr_in6 incm;
socklen_t ilen = sizeof incm;

ssize_t ret = recvfrom(sockfd, buf, sizeof buf, 0, (struct sockaddr *) &incm, &incm);
if (ret < 0) {
perror("Receive error");
return EXIT_FAILURE;
}

char addrbuf[INET6_ADDRSTRLEN];
if (!inet_ntop(AF_INET6, &incm.sin6_addr, addrbuf, sizeof addrbuf)) {
perror("Unable to convert source address to text form");
return EXIT_FAILURE;
}

printf("Incoming datagram from %s, port %hu:\n", addrbuf, ntohs(incm.sin6_port));

for (size_t iter = 0; iter < ret; ++iter)
printf("%02x", buf[iter]);
putchar('\n');

return EXIT_SUCCESS;
}

Scatter and gather

Multiplexing

By default, operations like connect, read, write or accept block the calling thread of execution until the operation could be completed. While this is acceptable for a programme handling only a single connection, it is problematic for a network daemon that must handle multiple connections.
A possible solution would be to spawn a new thread or process for every connection, but, although this method is very straight‐forward and easy to implement, it is highly inefficient and will not scale very well.
To alleviate this issue, we can mark a socket as non‐blocking so that all operations that would suspend the calling thread fail and set errno to EAGAIN or EWOULDBLOCK (both defined in <errno.h>). We could now try every socket in an infinite loop, but this naïve approach would keep the CPU constantly busy – and even if we were to suspend the process after every iteration, we would still waste precious ressources, as every read or write call incurs a costly context switch.
As the operating system handling the hardware access and low‐level protocol mechanisms knows when a certain operation is possible on a socket, it could notify the programme about it. This can be done using the I/O multiplexing facilities select and poll or by signal‐driven I/O. While the latter requires careful programming due to the asynchronous nature of signals, poll and select are quite easy to use.

Non‐blocking operation

Non‐blocking operation can be achieved by marking a socket as non‐blocking via the fcntl call or by specifying the MSG_DONTWAIT flag in a call to recvfrom or sendto. Of course, the latter will only affect the behaviour of the current operation.

#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ...);

Where fd specifies the (socket) file descriptor, cmd the operation to perform, followed by an operation‐dependent number of arguments. It will return 0 on success or -1 on failure and set errno accordingly.

To mark a socket as non‐blocking, one gives F_SETFL as operation, followed by the O_NONBLOCK flag:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
/* … */

if (fcntl(sockfd, F_SETFL, O_NONBLOCK)) {
perror("Failed to mark socket as non‐blocking");
return EXIT_FAILURE;
}

/* … */
return EXIT_SUCCESS;
}

select

#include <sys/select.h>

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

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);

select takes three sets (usually implemented as a bitmap) of file descriptors that can be operated on with the FD_‐macros. FD_CLR removes a descriptor from a set, FD_SET adds it, FD_ZERO removes all descriptors and FD_ISSET checks if a descriptor is in the set. It will return a non‐zero value if this is the case or 0 if not.
The readfds set contains the descriptors that should be checked for being ready to read, the writefds for those to be checked for being ready to write and the last, errorfds specifies those to be checked for having an error condition pending. Any of the sets may be specified as (fd_set *) 0, if one does not wish to check for a condition.
nfds specifies the range of file descriptors to be tested and select will only test the descriptors in the range of 0 to nfds-1.
The last argument, timeout gives the maximum time the select call should suspend the current thread of execution. A timeout value of (struct timeval *) 0 lets select suspend the current thread indefinitely until one of the conditions occur, while a pointer to a structure whose members are all 0 makes select return immediately.

Upon successful completion, select will have marked all the file descriptors in the three sets for which the given condition has occurred and cleared all others and return the total number of file descriptors marked. Otherwise, -1 is returned and errno is set accordingly to indicate the error.

Example

The following example creates an IPv6 stream socket, binds it to the any address and listens for incoming connections. The programme then waits for one of the sockets to become ready, receives any data and prints it.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h
#include <fcntl.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/select.h>

int main(int argc, char *argv[]) {
int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
return EXIT_FAILURE;
}

struct sockaddr_in6 addr;
memset(&addr, 0, sizeof addr);
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(1024);
addr.sin6_addr = in6addr_any;

if (bind(sockfd, (struct sockaddr *) &addr, sizeof addr)) {
perror("Could not bind socket");
return EXIT_FAILURE;
}


if (listen(sockfd, 128)) {
perror("Unable to listen for connections");
return EXIT_FAILURE;
}

if (fcntl(sockfd, F_SETFL, O_NONBLOCK)) {
perror("Failed to mark socket as non‐blocking");
return EXIT_FAILURE;
}

/* Set to hold all active connections */
fd_set all;
FD_ZERO(&all);
FD_SET(sockfd, &all);
int max = sockfd;

for (;;) {
fd_set test;
FD_ZERO(&test);

/* Copy set of active connections */
for (int fd = 0; fd <= max; ++fd)
if (FD_ISSET(fd, &all))
FD_SET(fd, &test);

int ret = select(max, &test, (fd_set *) 0, (fd_set *) 0, (struct timeval *) 0));
if (ret < 0) {
perror("Select failed");
return EXIT_FAILURE;
}

/* Handle all descriptors ready to be read */
for (int fd = 0; fd <= max && ret > 0; ++fd) {
/* Descriptor ready? */
if (FD_ISSET(fd, &test)) {
/* Listening socket or connection socket? */
if (fd == sockfd) {
/* Accept incoming connection */
int connfd = accept(sockfd, (struct sockaddr *) 0, (socklen_t *) 0);
if (connfd < 0) {
perror("Failed to accept incoming connection");
return EXIT_FAILURE;
}

if (fcntl(connfd, F_SETFL, O_NONBLOCK)) {
perror("Could not mark connection socket as non‐blocking");
return EXIT_FAILURE;
}

/* Add new descriptor to set */
FD_SET(connfd, &all);
if (fd > max)
max = fd;
}
else {
/* Read data */
char buf[256];
ssize_t ret = read(fd, buf, sizeof buf);
if (ret < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
perror("Read error");
return EXIT_FAILURE;
}
}
else {
for (size_t iter = 0; iter < ret; ++iter)
printf("%02x", buf[iter]);
putchar('\n');
}
}
--ret;
}
}
}
}

A sensible programmer would of course check for error conditions and close connections after they are not needed anymore. Also, it would be more efficient to accept and read exhaustively until an EAGAIN orEWOULDBLOCK error occurred, because select would return immediately in the following iteration if there were still data available for reading.

poll

poll performs a similar task to select, although it is more efficient under most circumstances, because one can reuse its data structures. However, unlike select, it is not available on Windows.

#include <poll.h>

struct pollfd {
int fd;
stort int events;
short int revents;
};

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

For each member of the array of struct pollfd structures, poll checks for any of the events specified by events on file descriptor fd and sets the revents field accordingly. nfds gives the number of members in the array, while timeout specifies the maximum time in milliseconds to suspend the calling thread. If the value of timeout is -1, it will block indefinitely until one of the events occurs or, if 0 is given, it will return immediately.

The events and revents fields are bitmasks constructed by OR‐ing a combination of the following flags:

POLLINThe descriptor is ready for readingPOLLOUTThe descriptor is ready for writing

Additionally, the following flags may occur in revents (they are ignored in events):

POLLERRAn error occurredPOLLHUPThe peer hung upPOLLNVALThe file descriptor is invalid

<poll.h> also defines other flags for handling priority data.

Upon success, poll returns the number of file descriptors for which at least one event occurred. On failure, it returns -1 and sets errno accordingly.

System‐specific mechanisms

While poll and select are available on nearly any system, they do not scale very well, because all kernel state has to be created anew every time they are called. There are system‐specific I/O multiplexing mechanisms like epoll (Linux) or kqueue (most BSDs) that alleviate this problem by keeping some kind of state in the kernel and thus scaling with the number of events occurring.
There exist libraries like libev that wrap these system‐specific mechanisms into a common interface to allow writing efficient programmes without sacrificing portability.