public inbox for devel@edk2.groups.io
 help / color / mirror / Atom feed
From: Laszlo Ersek <lersek@redhat.com>
To: Brijesh Singh <brijesh.singh@amd.com>, edk2-devel@lists.01.org
Cc: Jordan Justen <jordan.l.justen@intel.com>,
	Tom Lendacky <thomas.lendacky@amd.com>,
	Ard Biesheuvel <ard.biesheuvel@linaro.org>
Subject: Re: [PATCH v1 04/14] OvmfPkg/VirtioLib: Add SharedBuffer helper functions
Date: Wed, 9 Aug 2017 22:30:41 +0200	[thread overview]
Message-ID: <9e743ec8-f2fe-65f2-9594-4731d2481a88@redhat.com> (raw)
In-Reply-To: <1502107139-412-5-git-send-email-brijesh.singh@amd.com>

On 08/07/17 13:58, Brijesh Singh wrote:
> Patch adds convenience helper functions to call VIRTIO_DEVICE_PROTOCOL
> AllocateSharedPages, FreeSharedPages, MapSharedBuffer and UnmapSharedBuffer
> member functions.
> 
> Cc: Ard Biesheuvel <ard.biesheuvel@linaro.org>
> Cc: Jordan Justen <jordan.l.justen@intel.com>
> Cc: Tom Lendacky <thomas.lendacky@amd.com>
> Cc: Laszlo Ersek <lersek@redhat.com>
> Contributed-under: TianoCore Contribution Agreement 1.1
> Signed-off-by: Brijesh Singh <brijesh.singh@amd.com>
> ---
>  OvmfPkg/Include/Library/VirtioLib.h   | 143 +++++++++++++
>  OvmfPkg/Library/VirtioLib/VirtioLib.c | 220 ++++++++++++++++++++
>  2 files changed, 363 insertions(+)

I prefer to add functions to VirtioLib that actually buy us something.
So let me evaluate that for each function:

> diff --git a/OvmfPkg/Include/Library/VirtioLib.h b/OvmfPkg/Include/Library/VirtioLib.h
> index 5badfb32917f..fa82734f1852 100644
> --- a/OvmfPkg/Include/Library/VirtioLib.h
> +++ b/OvmfPkg/Include/Library/VirtioLib.h
> @@ -3,6 +3,7 @@
>    Declarations of utility functions used by virtio device drivers.
>  
>    Copyright (C) 2012-2016, Red Hat, Inc.
> +  Copyright (C) 2017, AMD Inc, All rights reserved.<BR>
>  
>    This program and the accompanying materials are licensed and made available
>    under the terms and conditions of the BSD License which accompanies this
> @@ -235,4 +236,146 @@ Virtio10WriteFeatures (
>    IN OUT UINT8                  *DeviceStatus
>    );
>  
> +/**
> +  Helper function to allocate pages that is suitable for sharing with
> +  hypervisor.
> +
> +  @param[in]  VirtIo  The target virtio device to use. It must be valid.
> +
> +  @param[in]  Pages   The number of pages to allocate.
> +
> +  @param[out] Buffer  A pointer to store the base system memory address of
> +                      the allocated range.
> +
> +  return              Returns error code from VirtIo->AllocateSharedPages()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioAllocateSharedPages (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  UINTN                   NumPages,
> +  OUT VOID                    **Buffer
> +  );
> +
> +/**
> +  Helper function to free pages allocated using VirtioAllocateSharedPages().
> +
> +  @param[in]  VirtIo  The target virtio device to use. It must be valid.
> +
> +  @param[in]  Pages   The number of allocated pages.
> +
> +  @param[in]  Buffer  System memory address allocated from
> +                      VirtioAllocateSharedPages ().
> +**/
> +VOID
> +EFIAPI
> +VirtioFreeSharedPages (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  UINTN                   NumPages,
> +  IN  VOID                    *Buffer
> +  );
> +
> +/**
> +  A helper function to map a system memory to a shared bus master memory for
> +  read operation from DMA bus master.
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in]  HostAddress     The system memory address to map to shared bus
> +                              master address.
> +
> +  @param[in]  NumberOfBytes   Number of bytes to be mapped.
> +
> +  @param[out] DeviceAddress   The resulting shared map address for the bus
> +                              master to access the hosts HostAddress.
> +
> +  @param[out] Mapping         A resulting value to pass to Unmap().
> +
> +  return                      Returns error code from
> +                              VirtIo->MapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioMapSharedBufferRead (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  VOID                    *HostAddress,
> +  IN  UINTN                   NumberOfBytes,
> +  OUT EFI_PHYSICAL_ADDRESS    *DeviceAddress,
> +  OUT VOID                    **Mapping
> +  );
> +
> +/**
> +  A helper function to map a system memory to a shared bus master memory for
> +  write operation from DMA bus master.
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in]  HostAddress     The system memory address to map to shared bus
> +                              master address.
> +
> +  @param[in]  NumberOfBytes   Number of bytes to be mapped.
> +
> +  @param[out] DeviceAddress   The resulting shared map address for the bus
> +                              master to access the hosts HostAddress.
> +
> +  @param[out] Mapping         A resulting value to pass to Unmap().
> +
> +  return                      Returns error code from
> +                              VirtIo->MapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioMapSharedBufferWrite (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  VOID                    *HostAddress,
> +  IN  UINTN                   NumberOfBytes,
> +  OUT EFI_PHYSICAL_ADDRESS    *DeviceAddress,
> +  OUT VOID                    **Mapping
> +  );
> +
> +/**
> +  A helper function to map a system memory to a shared bus master memory for
> +  common operation from DMA bus master.
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in]  HostAddress     The system memory address to map to shared bus
> +                              master address.
> +
> +  @param[in]  NumberOfBytes   Number of bytes to be mapped.
> +
> +  @param[out] Mapping         A resulting value to pass to Unmap().
> +
> +  return                      Returns error code from
> +                              VirtIo->MapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioMapSharedBufferCommon (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  VOID                    *HostAddress,
> +  IN  UINTN                   NumberOfBytes,
> +  OUT VOID                    **Mapping
> +  );
> +
> +/**
> +  A helper function to unmap shared bus master memory mapped using Map().
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in] Mapping          A mapping value return from Map().
> +
> +  return                      Returns error code from
> +                              VirtIo->UnmapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioUnmapSharedBuffer (
> +  IN VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN VOID                    *Mapping
> +  );
>  #endif // _VIRTIO_LIB_H_
> diff --git a/OvmfPkg/Library/VirtioLib/VirtioLib.c b/OvmfPkg/Library/VirtioLib/VirtioLib.c
> index 845f206369a3..f6b464b6cdf8 100644
> --- a/OvmfPkg/Library/VirtioLib/VirtioLib.c
> +++ b/OvmfPkg/Library/VirtioLib/VirtioLib.c
> @@ -4,6 +4,7 @@
>  
>    Copyright (C) 2012-2016, Red Hat, Inc.
>    Portion of Copyright (C) 2013, ARM Ltd.
> +  Copyright (C) 2017, AMD Inc, All rights reserved.<BR>
>  
>    This program and the accompanying materials are licensed and made available
>    under the terms and conditions of the BSD License which accompanies this
> @@ -414,3 +415,222 @@ Virtio10WriteFeatures (
>  
>    return Status;
>  }
> +
> +/**
> +  Helper function to allocate pages that is suitable for sharing with
> +  hypervisor.
> +
> +  @param[in]  VirtIo  The target virtio device to use. It must be valid.
> +
> +  @param[in]  Pages   The number of pages to allocate.
> +
> +  @param[out] Buffer  A pointer to store the base system memory address of
> +                      the allocated range.
> +
> +  return              Returns error code from VirtIo->AllocateSharedPages()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioAllocateSharedPages (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  UINTN                   NumPages,
> +  OUT VOID                    **Buffer
> +  )
> +{
> +  return VirtIo->AllocateSharedPages (VirtIo, NumPages, Buffer);
> +}

(1) I think this function buys us nothing. I suggest to drop it.

> +
> +/**
> +  Helper function to free pages allocated using VirtioAllocateSharedPages().
> +
> +  @param[in]  VirtIo  The target virtio device to use. It must be valid.
> +
> +  @param[in]  Pages   The number of allocated pages.
> +
> +  @param[in]  Buffer  System memory address allocated from
> +                      VirtioAllocateSharedPages ().
> +**/
> +VOID
> +EFIAPI
> +VirtioFreeSharedPages (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  UINTN                   NumPages,
> +  IN  VOID                    *Buffer
> +  )
> +{
> +  VirtIo->FreeSharedPages (VirtIo, NumPages, Buffer);
> +}

(2) Same here.

> +
> +STATIC
> +EFI_STATUS
> +VirtioMapSharedBuffer (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  VIRTIO_MAP_OPERATION    Operation,
> +  IN  VOID                    *HostAddress,
> +  IN  UINTN                   NumberOfBytes,
> +  OUT EFI_PHYSICAL_ADDRESS    *DeviceAddress,
> +  OUT VOID                    **Mapping
> +  )

(3) Please add a comment block above the function. Basically you can
copy the MapSharedBuffer() documentation, just point out the extra logic
for NumberOfBytes.

In fact, please rename this function to:

  VirtioMapAllBytesInSharedBuffer

> +{
> +  EFI_STATUS            Status;
> +  VOID                  *MapInfo;
> +  UINTN                 Size;
> +  EFI_PHYSICAL_ADDRESS  PhysicalAddress;
> +
> +  Size = NumberOfBytes;
> +  Status = VirtIo->MapSharedBuffer (
> +                     VirtIo,
> +                     Operation,
> +                     HostAddress,
> +                     &Size,
> +                     &PhysicalAddress,
> +                     &MapInfo
> +                     );
> +  if (EFI_ERROR (Status)) {
> +    return Status;
> +  }
> +
> +  if (Size < NumberOfBytes) {
> +    goto Failed;
> +  }
> +
> +  *Mapping = MapInfo;
> +  *DeviceAddress = PhysicalAddress;
> +
> +  return EFI_SUCCESS;
> +Failed:
> +  VirtIo->UnmapSharedBuffer (VirtIo, MapInfo);
> +  return EFI_OUT_OF_RESOURCES;
> +}

Now, I think this helper function does actually buy us convenience -- it
helps us centralize error handling, and it allows callers to pass in
constants or other evaluated expressions for NumberOfBytes.

I've given quite a bit of thought to our "hardliner" handling of the
case when NumberOfBytes is decreased on output. The PciIo spec says that
in such cases the transfer should be broken up into smaller chunks:

> In all mapping requests the resulting NumberOfBytes actually mapped
> may be less than the requested amount. In this case, the DMA operation
> will have to be broken up into smaller chunks. The Map() function will
> map as much of the DMA operation as it can at one time. The caller may
> have to loop on Map() and Unmap() in order to complete a large DMA
> transfer.

While generally speaking that could be a valid idea, I think it doesn't
apply well to virtio, for the following reasons:

- I don't think that CommonBuffer areas are possible to split up in any
sensible way (e.g., rings),

- Even for unidirectional transfers, the request framing is usually
dictated on the virtio request level (i.e., request framing is usually
matched closely by the individual virtio descriptors that constitute the
descriptor chain that *is* the virtio request). If PciIo (ultimately:
the IOMMU) "suggests" that we split the request into smaller chunks,
that could affect the descriptors / descriptor chaining too, and I
simply don't want to go there.

- Turning a single "outer request" (like EFI_BLOCK_IO_PROTOCOL read or
write request) into a series of "inner virtio requests" is a mess as
well -- what if you get a failure from the hypervisor mid-loop?

IMO, we should forward the outer (higher level) request as transparently
over virtio as possible. If the IOMMU limits that for whatever reason,
then we should fail the request immediately, and propagate the error to
the client of the higher level protocol.

So, IMO, the handling of NumberOfBytes is valid here, and this function
is useful.

(4) Please make this function extern (add it to the header file too).

> +
> +/**
> +  A helper function to map a system memory to a shared bus master memory for
> +  read operation from DMA bus master.
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in]  HostAddress     The system memory address to map to shared bus
> +                              master address.
> +
> +  @param[in]  NumberOfBytes   Number of bytes to be mapped.
> +
> +  @param[out] DeviceAddress   The resulting shared map address for the bus
> +                              master to access the hosts HostAddress.
> +
> +  @param[out] Mapping         A resulting value to pass to Unmap().
> +
> +  return                      Returns error code from
> +                              VirtIo->MapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioMapSharedBufferRead (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  VOID                    *HostAddress,
> +  IN  UINTN                   NumberOfBytes,
> +  OUT EFI_PHYSICAL_ADDRESS    *DeviceAddress,
> +  OUT VOID                    **Mapping
> +  )
> +{
> +  return VirtioMapSharedBuffer (VirtIo, EfiVirtIoOperationBusMasterRead,
> +           HostAddress, NumberOfBytes, DeviceAddress, Mapping);
> +}

(5) I don't think this function buys us much. The only parameter you can
save at the call site is Operation, but the rest (together with the
saving of the return code) will require so many characters still that
you'll have to break it to multiple lines anyway. So I suggest to drop
this function.

> +
> +/**
> +  A helper function to map a system memory to a shared bus master memory for
> +  write operation from DMA bus master.
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in]  HostAddress     The system memory address to map to shared bus
> +                              master address.
> +
> +  @param[in]  NumberOfBytes   Number of bytes to be mapped.
> +
> +  @param[out] DeviceAddress   The resulting shared map address for the bus
> +                              master to access the hosts HostAddress.
> +
> +  @param[out] Mapping         A resulting value to pass to Unmap().
> +
> +  return                      Returns error code from
> +                              VirtIo->MapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioMapSharedBufferWrite (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  VOID                    *HostAddress,
> +  IN  UINTN                   NumberOfBytes,
> +  OUT EFI_PHYSICAL_ADDRESS    *DeviceAddress,
> +  OUT VOID                    **Mapping
> +  )
> +{
> +  return VirtioMapSharedBuffer (VirtIo, EfiVirtIoOperationBusMasterWrite,
> +           HostAddress, NumberOfBytes, DeviceAddress, Mapping);
> +}

(6) Same as (5) here.

> +
> +/**
> +  A helper function to map a system memory to a shared bus master memory for
> +  common operation from DMA bus master.
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in]  HostAddress     The system memory address to map to shared bus
> +                              master address.
> +
> +  @param[in]  NumberOfBytes   Number of bytes to be mapped.
> +
> +  @param[out] Mapping         A resulting value to pass to Unmap().
> +
> +  return                      Returns error code from
> +                              VirtIo->MapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioMapSharedBufferCommon (
> +  IN  VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN  VOID                    *HostAddress,
> +  IN  UINTN                   NumberOfBytes,
> +  OUT VOID                    **Mapping
> +  )
> +{
> +  EFI_STATUS            Status;
> +  EFI_PHYSICAL_ADDRESS  DeviceAddress;
> +
> +  Status = VirtioMapSharedBuffer (VirtIo,
> +             EfiVirtIoOperationBusMasterCommonBuffer, HostAddress,
> +             NumberOfBytes, &DeviceAddress, Mapping);
> +
> +  //
> +  // On Success, lets verify that DeviceAddress same as HostAddress
> +  //
> +  if (!EFI_ERROR (Status)) {
> +    ASSERT (DeviceAddress == (EFI_PHYSICAL_ADDRESS) (UINTN) HostAddress);
> +  }
> +
> +  return Status;
> +}

So this function seems to save us two parameters:

(7) Operation -- I commented on that under (5).

(8) DeviceAddress -- I actually disagree with hiding that. In my
opinion, this is different from the NumberOfBytes question.

We can be inflexible with NumberOfBytes because the in-out NumberOfBytes
parameter of PciIo.Map() reflects an *accidental* trait of some IOMMUs
(namely that their map sizes may be limited). I think that in the virtio
scope we can safely say that we simply don't support IOMMU protocol
instances that can't accommodate our desired request framing.

(Case in point: versions of the virtio spec have presented limits on
virtio request sizes, and thus the virtio drivers already enforce
suitable (stricter) limits on the outer protocol interfaces. Search
"VirtioBlk.c" and "VirtioScsi.c" for SIZE_1GB, for example. Therefore
refusing oversized requests is nothing new, except the definition of
"oversized" might vary, dependent on IOMMU.)

However, DeviceAddress being (potentially) different from HostAddress is
the core idea (an *inherent* trait) of the IOMMU protocol. If we ever
implement a different IOMMU protocol -- e.g. for QEMU's vIOMMU --, I
wouldn't like this assert -- or function prototype -- to cause issues.

So, please drop this helper function, and update all call sites to pass
DeviceAddress to the virtio device.

(I'm aware that this might cause major churn, and more review work for
me, but after all, this is the entire point of the IOMMU abstraction.)

> +
> +/**
> +  A helper function to unmap shared bus master memory mapped using Map().
> +
> +  @param[in]  VirtIo          The target virtio device to use. It must be
> +                              valid.
> +
> +  @param[in] Mapping          A mapping value return from Map().
> +
> +  return                      Returns error code from
> +                              VirtIo->UnmapSharedBuffer()
> +**/
> +EFI_STATUS
> +EFIAPI
> +VirtioUnmapSharedBuffer (
> +  IN VIRTIO_DEVICE_PROTOCOL  *VirtIo,
> +  IN VOID                    *Mapping
> +  )
> +{
> +  return VirtIo->UnmapSharedBuffer (VirtIo, Mapping);
> +}
> 

(9) I think this function also doesn't buy us much, please drop it.

Thanks,
Laszlo


  reply	other threads:[~2017-08-09 20:28 UTC|newest]

Thread overview: 42+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2017-08-07 11:58 [PATCH v1 00/14] OvmfPkg/Virtio: Add APIs to map system physical to device address Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 01/14] OvmfPkg/Virtio: Introduce new member functions in VIRTIO_DEVICE_PROTOCOL Brijesh Singh
2017-08-09 14:27   ` Laszlo Ersek
2017-08-09 18:23     ` Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 02/14] OvmfPkg/Virtio10Dxe: Implement new member functions Brijesh Singh
2017-08-09 16:50   ` Laszlo Ersek
2017-08-07 11:58 ` [PATCH v1 03/14] OvmfPkg/VirtioPciDeviceDxe: " Brijesh Singh
2017-08-09 17:09   ` Laszlo Ersek
2017-08-10 18:41     ` Brijesh Singh
2017-08-10 19:47       ` Laszlo Ersek
2017-08-07 11:58 ` [PATCH v1 04/14] OvmfPkg/VirtioLib: Add SharedBuffer helper functions Brijesh Singh
2017-08-09 20:30   ` Laszlo Ersek [this message]
2017-08-07 11:58 ` [PATCH v1 05/14] OvmfPkg/VirtioLib: Pass VirtIo instance in VringInit/Uinit() Brijesh Singh
2017-08-09 21:13   ` Laszlo Ersek
2017-08-07 11:58 ` [PATCH v1 06/14] OvmfPkg/VirtioLib: Add functions to map/unmap VRING Brijesh Singh
2017-08-09 23:51   ` Laszlo Ersek
2017-08-07 11:58 ` [PATCH v1 07/14] OvmfPkg/VirtioLib: Use AllocateShared() to allocate vring buffer Brijesh Singh
2017-08-10  0:02   ` Laszlo Ersek
2017-08-07 11:58 ` [PATCH v1 08/14] OvmfPkg/VirtioBlkDxe: Use DeviceAddresses in vring descriptors Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 09/14] OvmfPkg/VirtioScsiDxe: " Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 10/14] OvmfPkg/VirtioNetDxe: Allocate Tx and Rx ring using AllocateSharedPage() Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 11/14] OvmfPkg/VirtioNetDxe: Allocate RxBuf using AllocateSharedPages() Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 12/14] OvmfPkg/VirtioNetDxe: Dynamically allocate transmit header Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 13/14] OvmfPkg/VirtioNetDxe: Use DeviceAddress in transmit vring descriptors Brijesh Singh
2017-08-07 11:58 ` [PATCH v1 14/14] OvmfPkg/VirtioRngDxe: Use DeviceAddresses in " Brijesh Singh
2017-08-10  0:25   ` Laszlo Ersek
2017-08-10  0:46     ` Laszlo Ersek
2017-08-09 14:39 ` [PATCH v1 00/14] OvmfPkg/Virtio: Add APIs to map system physical to device address Laszlo Ersek
2017-08-09 17:35   ` Brijesh Singh
2017-08-09 17:56     ` Laszlo Ersek
2017-08-09 19:29       ` Laszlo Ersek
2017-08-11 22:22       ` Brijesh Singh
2017-08-15 10:42         ` Laszlo Ersek
2017-08-15 19:32           ` Brijesh Singh
2017-08-15 19:48             ` Laszlo Ersek
2017-08-15 20:26               ` Brijesh Singh
2017-08-15 20:39                 ` Laszlo Ersek
2017-08-15 20:44                   ` Brijesh Singh
2017-08-15 21:57                     ` Laszlo Ersek
2017-08-09 22:38 ` Laszlo Ersek
2017-08-09 22:44   ` Brijesh Singh
2017-08-10  9:53     ` Laszlo Ersek

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-list from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=9e743ec8-f2fe-65f2-9594-4731d2481a88@redhat.com \
    --to=devel@edk2.groups.io \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox