Figure 617 BIO lists evolving under recursive calls to genericmakerequest

current->bio_tail is initialized to null, so we can skip the first conditional block. One bio instance is submitted, and the data structures look as in Figure 6-17(a). bio points to the submitted BIO, while current->bio_list is NULL and current_bio_tail point to the address of current_bio_tail. Note that the following pictures in the figure will always consider the local bio variable of the first function call to generic_make_request — not any variables in later stack frames.

Now suppose that_generic_make_request does recursively call generic_make_request to submit a BIO instance, which we call BIO 2. How do the data structures look when_generic_make_request returns? Consider the action of the recursive call: Since current->bio_tail is not a null pointer anymore, the initial if-block in generic_make_request is processed. current->bio_list then points to the second BIO, and current->bio_tail points at the address of the bi_next of BIO 2. Thus the data structure looks as in Figure 6-17(b) when_generic_make_request returns.

The do loop is now executed a second time. Before the second — iterative! — call to_generic_make_

request, the data structure looks as in Figure 6-17(c), and the second bio instance is processed. If no more BIOs are submitted recursively, the job is done afterward.

The method also works if_generic_make_request calls generic_make_request more than once. Imagine that three additional BIOs are submitted. The resulting data structure is depicted in Figure 6-17(d). If no more BIOs are submitted afterward, the loop processes the existing BIO instances one after another and then returns.

After having resolved the difficulties with recursive generic_make_request calls, we can go on to examine the default make_request_fn implementation_make_request. Figure 6-18 shows the code flow diagram.13

Figure 6-18: Code flow diagram for_make_request.

Once the information needed to create the request has been read from the passed bio instance, the kernel invokes elv_queue_empty to check whether the elevator queue is currently empty. If so, work is made easier as there is no need to merge the request with existing requests (because none is present).

If there are pending requests in the queue, elv_merge is called to invoke the elevator_merge_fn function of the elevator element associated with the request queue (Section 6.5.8 deals with the implementation of I/O schedulers). At this point, we are interested only in the result of the function. It returns a pointer to the request list position at which the new request is to be inserted. The I/O scheduler also specifies whether and how the request is to be coalesced with existing requests.

13As a slight simplification, I omit bounce buffer handling. Older hardware might only be able to transfer data into some specific region in memory. In this case, the kernel initiates the transfer to go into this region and copies the result to some more apt place after the transfer is finished.

□ elevator_back_merge and elevator_front_merge cause the new request to be coalesced with the request at the found position in the request list. elevator_back_merge merges the new data after the data of the existing request, elevator_front_merge before the data.

The existing element is modified to produce a merged request covering the desired areas.

□ elevator_no_merge finds out that the request cannot be coalesced with existing elements on the request queue and must therefore be added on its own.

These are the only actions that an elevator can take; it cannot influence the request queue in any other way. This clearly demonstrates the difference between I/O and CPU schedulers. Although both are faced with a very similar problem, the solutions they provide diverge greatly.

Once the elevator requirements have been satisfied (as far as this is possible), the kernel must generate a new request.

get_request_wait allocates a new request instance that is then filled with data from the bio using init_request_from_bio. If the queue is still empty (this is checked by elv_queue_empty), it is plugged with blk_plug_device. This is how the kernel prevents the queue from being processed after each new request; instead, it collects requests for read and/or write operations and executes them in a single action. I discuss the mechanism used shortly.

After a few kernel statistics have been updated with_elv_add_request_pos, add_request adds the request to the request list (this leads to the I/O scheduler-specific elevator_add_req_fn function) at the position selected by the above I/O scheduler call.

If a request is to be processed synchronously (bio_rw_sync must then be set in the bio instance of the request), the kernel must unplug the queue using_generic_unplug_device to ensure that the request can, in fact, be handled synchronously. Requests of this kind are seldom used because they negate the effect of I/O scheduling.

Continue reading here: Queue Plugging

Was this article helpful?

0 0