In the previous article, we talked about three methods to receive notifications of the completion of asynchronous I/O requests: notifying a device kernel object, notifying an event kernel object, and alerting I/O.
This article mainly introduces another method to accept asynchronous I/O requests-the I/O completion port. This is the best method with the highest performance and scalability. However, the implementation is complicated.
Two server thread models are introduced before I/O Completion Ports are introduced:
- Continuous model: a single thread waits for a customer's request. Once a customer sends a request, the thread wakes up and processes the customer's request.
- Concurrency model: a single thread waits for a customer's request. Once a customer sends a request, the thread creates another thread to process the request. While the newly created thread processes the request, the original thread waiting for the request continues to wait for the request of another customer through a loop. After the request processing thread completes, it is automatically destroyed.
The biggest disadvantage of the continuous model is that it cannot process multiple requests at the same time. It can only wait, process, wait, process ...... This works in turn. When two requests arrive at the same time, they can only process one of them, and 2nd requests must wait until 1st requests are processed. Ping the server is a typical continuous model.
The concurrency model allows a thread to wait for a request. This thread can create a thread for each request to process it. The advantage is that the thread waiting for the request does little work, and the default status is blocking. When a customer request arrives, the thread is awakened and a new thread is created to process the request. Then the thread continues to wait for another request. In this way, when multiple customer requests arrive at the same time, they can be processed almost simultaneously. However, when there are too many client requests, there will be too many processing threads which can be scheduled, so there will be many "thread conversions, the Windows Kernel will spend a lot of time on "thread conversion", which wastes a lot of time. To solve this problem, Windows provides the "I/O completion port" kernel object.
Imagine that if some threads are created in advance to put these threads in the waiting state, and then all user requests are delivered to a message queue, then these threads are awakened, by extracting and processing requests from the Message Queue one by one, you can avoid opening up threads for each user, saving resources and improving thread utilization. In fact, the I/O completion port is based on this idea. It seems like a "message queue", which is not closely related to its own name "I/O completion port.
Create an I/O completion port
The I/O completion port is called the most complex Kernel Object. You can use createiocompletionport to create an I/O completion port kernel object:
Handle createiocompletionport (
Handle hfile, // device handle
Handle hexistingcompletionport, // The created I/O completed port object handle
Ulong_ptr completionkey, // a completion key, which is equivalent to a completion key
DWORD dwnumberofconcurrentthreads); // Number of threads allowed to run simultaneously
It is hard to understand this function. In fact, this function has two functions: Create an I/O completion port and associate an I/O completion port with a device. Therefore, you can split the function. The following createnewcompletionport function is used to create an I/O completion port:
Handle createnewcompletionport (DWORD dwnumberofconcurrentthreads)
{
Return (createiocompletionport (invalid_handle_value, null, 0,
Dwnumberofconcurrentthreads ));
}
This function accepts a parameter, calls createiocompletionport internally, and sets the first three parameters to invalid_handle_vlaue, null, and 0. The last parameter is retained to the user, so an I/O completion port is created. The dwnumberofconcurrentthreads parameter tells the I/O completion port how many threads can be executed currently. If 0 is passed, indicates that there is no limit on the number of threads allowed for execution. This parameter is used to prevent too frequent "thread switching. You can dynamically increase its value to test a reasonable number of runable threads to achieve optimal performance.
Associate I/O Completion Ports with devices
When you create an I/O completion port, the kernel actually creates five data structures:
1. device list: the device associated with the created I/O completion port
2. I/O request Completion queue (FIFO ):
3. Waiting for thread Queue (LIFO)
4. release thread list
5. Pause thread list
1st data structures: the device list specifies the device associated with the I/O completion port, which can be one device or multiple devices. You can use the createiocompletionport function to associate the device with the I/O port, or you can split the function by using the following functions:
Bool associatedevicewithcompletionport (
Handle hcompletionport, // I/O completed port kernel object handle
Handle hdevice, // device kernel object handle
DWORD dwcompletionkey) // complete the key
{
Handle H = createiocompletionport (hdevice, hcompletionport,
Dwcompletionkey, 0 );
Return (H = hcompletionport );
}
This function provides an I/O finished port handle and a device handle, and associates the two. The last parameter is a completion key. This value is useful when Processing notifications of I/O requests. It is only meaningful to you and the system will not pay attention to it.
Each time you call this operation, the system adds a record to the data structure "Device List" of the I/O completion port, this record specifies the device associated with this I/O completion port.
Because the createiocompletionport function is complex, we recommend that you split it, or you can also create an I/O port and associate it with the device. The Code is as follows:
# Define ck_file 1
Handle hfile = create (...);
// Create an I/O completion port and associate the device most represented by hfile. The number of runable threads is 2.
Handle hcompletionport = createcompletionport (hfile, null, ck_file, 2 );
The 2nd data structure is "I/O request Completion queue ". When an asynchronous device's I/O request is complete, the system checks whether the device is associated with an I/O completed port. If yes, the system adds a "completed I/O requests" record at the end of the "I/O request Completion queue. Each record in the queue indicates the following content: 1. Number of bytes of transmitted data; 2. Completion key when the device is associated with the I/O completion port; 3. overlapped structure pointer of the I/O Request; 4. An error code.
Obtain the status of the I/O completed Port
When your server system starts, you should create an I/O completion port and then create a thread pool to process customer requests. Generally, the number of threads in the thread pool is twice that of the CPU.
All threads in the thread pool perform the same functions. These threads are often blocked to wait for device I/O to complete. You can use the getqueuedcompletionstatus function to implement the following functions:
Bool getqueuedcompletionstatus (
Handle hcompletionport, // I/O completed port object handle
Pdword pdwnumberofbytestransferred, // number of bytes for data transmission
Pulong_ptr pcompletionkey, // The associated completion key
Overlapped ** ppoverlapped, // address of the overlapped structure pointer
DWORD dwmilliseconds); // wait time (MS)
This function allows the thread to wait for a specific I/O completion port and specify the I/O completion port through the first parameter. This function makes the thread that calls it wait until a record appears in the "I/O request Completion queue" of the I/O completion port, or the time specified by the dwmilliseconds parameter exceeds.
3rd data structures: "waiting for thread queue", indicating all threads waiting for this I/O completion port, these threads are waiting for an I/O completion port because they call the getqueuedcompletionstatus function. The IDS of these threads are recorded in this queue, so that the I/O completion port can know which threads are waiting. When a device associated with an I/O completion port completes an asynchronous device I/O request, a record appears at the end of the "I/O request Completion queue, at this time, the I/O completion port is wakened to a thread in the "Waiting thread queue". The getqueuedcompletionstatus function called by this thread will return, obtain the number of bytes of the transmitted data, the address of the completed key, and the overlapped structure.
The reason for determining whether the getqueuedcompletionstatus function returns is complex. You can use the following encoding to determine the cause:
DWORD dwnumbytes; // number of bytes for data transmission
Ulong_ptr completionkey; // The completion key.
Overlapped * poverlapped; // overlapped structure pointer
// Hiocp is an I/O completed port object handle, which is created elsewhere
Bool Bok = getqueuedcompletionstatus (hiocp,
& Dwnumbytes, & completionkey, & poverlapped, 1000 );
DWORD dwerror = getlasterror (); // get the error code
If (Bok)
{
// Wait until the request succeeds. an I/O request is completed and can be processed.
}
Else
{
If (poverlapped! = NULL)
{
// I/O request failed. The dwerror Error Code contains the error cause.
}
Else
{
If (dwerror = wait_timeout)
{
// The wait time exceeds and no records appear in the "I/O request Completion queue"
}
Else
{
// Call getqueuedcompletionstatus incorrectly. For example, the handle is invalid.
// The dwerror Error Code contains the cause of the error.
}
}
}
Note that the records in the "I/O request Completion queue" are in FIFO mode. The threads in the "waiting for thread queue" are in and out in LIFO mode, much like a stack (but the author said it was Queue ).
In Windows Vista, If you want many I/O requests to be submitted or processed at the same time, you do not need to add many threads, you can use getqueuedcompletionstatusex to obtain the results of multiple I/O requests:
Bool getqueuedcompletionstatusex (
Handle hcompletionport, // I/O completion port handle
Lpoverlapped_entry pcompletionportentries, // I/O request Completion record Array
Ulong ulcount, // number of completed records of I/O requests
Pulong pulnumentriesremoved, // actual acquired I/O request Completion record
DWORD dwmilliseconds, // wait time
Bool balertable); // specifies whether to enable the thread to enter the "STANDBY state". This parameter is generally set to false.
The 2nd parameters of this function are an address pointing to the overlapped_entry structure (generally an array of this structure). The structure is defined as follows:
Typedef struct _ overlapped_entry {
Ulong_ptr lpcompletionkey; // The completion key.
Lpoverlapped; // overlapped pointer
Ulong_ptr internal; // this field should be avoided
DWORD dwnumberofbytestransferred; // number of bytes for data transmission
} Overlapped_entry, * lpoverlapped_entry;
This book contains a section "How the I/O completion port manages the thread pool
", I don't think it's necessary to talk about it. Just look at it. It's all internal details.
The number of threads in the thread pool should also be discussed. I have read some materials. This book is about twice the number of CPUs, and there are also some materials about the number of 2 * CPUs + 2. I feel that there is nothing to talk about. Let's analyze the specific problems, haha.
Imitating completed I/O requests
You can simulate a completed I/O Request to wake up and execute a thread waiting on the I/O completed port. This is also a mechanism for inter-thread communication. You can implement it through postqueuedcompletionstatus:
Bool postqueuedcompletionstatus (
Handle hcompletionport, // I/O finished object handle
DWORD dwnumbytes, // The number of bytes of data to be transmitted
Ulong_ptr completionkey, // complete key
Overlapped * poverlapped); // overlapped structure pointer
This function adds a record to the "I/O request Completion queue" of the I/O completion port, some data corresponding to this record is provided by the 2nd, 3, and 4 parameters of this function. If the call is successful, true is returned.
I/O to complete port usage steps
Taking network service socket as an example, I/O port usage steps are described as follows:
1. initialize the socket (ws2_32.dll) -- wsastartup
2. Create an I/O completion port
3. Create some threads that can contain one listening thread and several processing threads in the waiting state.
4. Create a Socket socket, bind the socket, and then listen (Listen)
5. Repeat and call accept to wait for the client to request a connection,
6. Associate the connected socket with the I/O completion port
7. To deliver a request for processing information, you can use postqueuedcompletionstatus to wake up the processing thread, so that the processing thread can process connection requests.
So repeated 5 ~ 7.