Before we go into IO multiplexing, we need to preview files and file descriptors.

What is a file

Programmers using I/O can’t escape files in the end.

Since this is also part of the high performance, high concurrency series, and Linux/Unix is indispensable for high performance, high concurrency, here is a discussion of files in the Linux world.

A file is actually a very simple concept for a programmer to think of as a sequence of N bytes:

Virtually all I/O devices are abstracted to the concept of files, Everything isFile, disks, network data, terminals, even pipes, etc. are treated as files.

All I/O operations are also implemented through file reads and writes, a very elegant abstraction that allows programmers to implement all I/O operations using a single set of interfaces.

Common I/O operation interfaces are as follows:

  • Open the file, open
  • Change the read/write position, seek
  • File read and write, read and write
  • Close the file, close

The power of the file concept is that programmers can perform almost any I/O operation through these interfaces.

File descriptor

In the second I/O section of this article, we will specify a buff to load data, such as disk data, like this:

read(buff);
Copy the code

But what we’re missing here is that even though we’re implementing where to write data to, where to read data from? As we know from the previous section, we can implement almost any I/O operation through the concept of files, so the one missing star here is files.

So how do we use files in general?

To compare the fire if you weekend restaurant should have experience, general this restaurant will line up over the weekend, and then the waiter will give you a line number, through the serial number attendant can find you, here is the benefits of the waiter don’t need to remember who you are, what’s your name, is to protect the environment love small animals, etc., The key point here is that the server knows nothing about you, but can still find you with a number.

Similarly, to use files in the Linux world, we need to use a number, which is called file descriptors according to the “don’t understand rule”, and is so well known in the Linux world for the same reason as the queue number above.

So, the file description is just a number, but we can manipulate an open file with this number, remember that.

With file descriptors, the process doesn’t know anything about the file, such as where the file is on the disk, how the file is managed in memory, etc. This information belongs to the operating system and the process doesn’t need to care. The operating system just gives the process a file descriptor.

So let’s improve the above procedure:

int fd = open(file_name);
read(fd, buff);
Copy the code

How about that? It’s very simple.

What if there are too many file descriptors

After so many fosters, finally to the high performance, high concurrency theme.

As we saw in the previous sections, all I/O operations can be done through the file-like concept, which of course includes network communication.

If you’re a Web server, after the three-way handshake, we call Accept and we get the same file descriptor, but the file descriptor is used for network communication, and you can read and write the file descriptor to communicate with the client. For conceptual purposes, we call this a link descriptor, which allows us to read and write data from the client.

int conn_fd = accept(...) ;Copy the code

The server’s processing logic is usually to read the client request data and then perform some specific logic:

if(read(conn_fd, request_buff) > 0) {
    do_something(request_buff);
}
Copy the code

It’s not that simple, but the world is complicated, and it’s not that simple.

Now comes the tricky part.

Since our topic is high concurrency, the server cannot communicate with just one client, but with thousands of clients. You’re not dealing with one descriptor, you’re dealing with tens of thousands of descriptors.

To avoid getting too complicated at first, let’s simplify and assume that only two client requests are processed at the same time.

Some of you might say, “Well, that’s not easy. I’ll just write it like this:

If (read(socket_fd1, buff) > 0) {// Handle first do_something(); } if(read(socket_fd2, buff) > 0) { do_something();Copy the code

We discussed in section 2 of this article that this is a very typical blocking I/O. If the first request is blocked and suspended, then we cannot process the second request, even though the second request is already in place, which means that all other clients must wait. And usually not just two clients but tens of thousands, tens of thousands of connections.

If you’re smart enough, you might want to use multiple threads, one for each request, so that if one thread is blocked, it won’t affect the other threads. Note that since the concurrency is high, we need to open thousands of threads for thousands of requests.

So how to solve this problem?

The key point here is that when we do I/O, we do not specify whether the I/O device is readable or writable. Doing I/O while the peripheral is unreadable or writable will only cause the process to block and be suspended.

So the elegant way to solve this problem is to think about it in a different way.

Don’t call me. I’ll call you if I need anything

You’re going to get more than one cold call in your life, ten or eight cold calls a day and it’s going to be draining.

The key to this scenario is that the caller doesn’t know if you want to buy something, only to ask you over and over again, so a better strategy is to not let them call you, take down their number and call them if you need to.

Which means don’t call me. I’ll call you if I need anything.

In this case, you are the kernel, the promoter is the application, the phone number is the file descriptor, and talking to you on the phone is the I/O.

As you can see by now, a better way to handle multiple file descriptors can be found in cold calls.

So instead of asking the kernel via the I/O interface if the peripherals corresponding to the file descriptors are ready, a better approach would be to throw them at the kernel and tell it: “I have 10,000 file descriptors here, you monitor them for me, you let me know when there are file descriptors that I can read and write so I can handle them.” Instead of weakly asking the kernel: “Is the first file description readable and writable? Is the second file descriptor readable and writable? Is the third file descriptor readable and writable?”

So the application goes from being “busy” active to being lazy passive, and the kernel notifies me which devices are ok anyway, so I don’t have to be so diligent when I can be lazy.

This is a different mechanism for handling I/O, again calling it I/O multiplexing.

I/O multiplexing I/O multiplexing

Multiplex the word multiplexing is actually used in the field of communication, in order to make full use of the communication line, want to transmit multiplex signal in a channel, want to transmit multiplex signal in a channel you need to combine the multiplex signal as a way, the multiplex signal combination into a signal equipment called multiplexer, Apparently the receiver needs to restore the original multiplexer after receiving the combined signal. This device is called demultiplexer, as shown in the picture:

Back to our subject.

I/O multiplexing refers to a process that:

  1. We get a bunch of file descriptors (network specific, disk file specific, etc., any file descriptor)
  2. By calling a function that tells the kernel, “Don’t return from this function, you’ll monitor the descriptors for me, and return when there’s something in the file descriptor that can do I/O.”
  3. When this function returns we know which file descriptors are available for I/O operations.

So what are the functions that can be used for I/O multiplexing?

There are three mechanisms for I/O multiplexing in the Linux world:

  • select
  • poll
  • epoll

Let’s take a brief look at the three awesome I/O multiplexers.

I/O multiplexing three musketeers

Essentially, select, poll, and epoll are blocking I/O, or synchronous I/O.

Select: novice

In the select I/O multiplexing mechanism, we need to tell SELECT the set of file descriptors that we want to monitor as function parameters, and then select will copy these file descriptors into the kernel. We know that data copying has a performance loss, so to reduce the performance loss caused by such data copying, we need to select the set of file descriptors that we want to monitor as function parameters. The Linux kernel do to set the size of the restrictions, and provisions of the user to monitor the file descriptor set no more than 1024, at the same time when the select return we can only know some file descriptors can read and write, but we don’t know is which one, so the programmer must traverse side again find the details of which file descriptor can read and write.

Therefore, select has the following characteristics:

  • I can look after a limited number of file descriptors, no more than 1024
  • The file descriptor that the user gave me needs to be copied in the kernel
  • I can only tell you that there are file descriptors that meet the requirements, but I don’t know which ones, you can find them one by one.

As you can see, the select feature is inefficient in high-performance web servers with tens of thousands of concurrent links.

Poll: A little makes a difference

Poll and SELECT are very similar, poll optimization over SELECT only solves the file descriptor limit of 1024. Select and poll both deteriorate as the number of monitored file descriptions increases, so they are not suitable for high concurrency scenarios.

Epoll: Beat the pack

Of the three problems that SELECT faces, the file description limit has been solved in poll, so what are the other two problems solved by epoll? You can answer this question by following the public account “Mynong’s Desert Island survival” and replying to “epoll”.

conclusion

Based on the design philosophy of “everything is a file”, I/O can also be implemented in the form of files. Obviously, high concurrency requires interaction with multiple files, which can not be separated from efficient I/O multiplexing technology. In this article, we explain in detail what IS I/O multiplexing and how to use it. Among them, I/O multiplexing (event-driven) technology represented by Epoll is widely used. In fact, you will find event-driven programming methods for high concurrency and high performance, which is also the topic of the next article, stay tuned.