From: Andrii Nakryiko <andriin@fb.com>
To: <bpf@vger.kernel.org>, <netdev@vger.kernel.org>, <ast@fb.com>,
<daniel@iogearbox.net>
Cc: <andrii.nakryiko@gmail.com>, <kernel-team@fb.com>,
Andrii Nakryiko <andriin@fb.com>,
"Paul E . McKenney" <paulmck@kernel.org>,
Jonathan Lemon <jonathan.lemon@gmail.com>
Subject: [PATCH v2 bpf-next 2/7] tools/memory-model: add BPF ringbuf MPSC litmus tests
Date: Sun, 17 May 2020 12:57:22 -0700 [thread overview]
Message-ID: <20200517195727.279322-3-andriin@fb.com> (raw)
In-Reply-To: <20200517195727.279322-1-andriin@fb.com>
Add 4 litmus tests for BPF ringbuf implementation, divided into two different
use cases.
First, two unbounded case, one with 1 producer and another with
2 producers, single consumer. All reservations are supposed to succeed.
Second, bounded case with only 1 record allowed in ring buffer at any given
time. Here failures to reserve space are expected. Again, 1- and 2- producer
cases, single consumer, are validated.
Just for the fun of it, I also wrote a 3-producer cases, it took *16 hours* to
validate, but came back successful as well. I'm not including it in this
patch, because it's not practical to run it. See output for all included
4 cases and one 3-producer one with bounded use case.
Each litmust test implements producer/consumer protocol for BPF ring buffer
implementation found in kernel/bpf/ringbuf.c. Due to limitations, all records
are assumed equal-sized and producer/consumer counters are incremented by 1.
This doesn't change the correctness of the algorithm, though.
Verification results:
/* 1p1c bounded case */
$ herd7 -unroll 0 -conf linux-kernel.cfg litmus-tests/mpsc-rb+1p1c+bounded.litmus
Test mpsc-rb+1p1c+bounded Allowed
States 2
0:rFail=0; 1:rFail=0; cx=0; dropped=0; len1=1; px=1;
0:rFail=0; 1:rFail=0; cx=1; dropped=0; len1=1; px=1;
Ok
Witnesses
Positive: 3 Negative: 0
Condition exists (0:rFail=0 /\ 1:rFail=0 /\ dropped=0 /\ px=1 /\ len1=1 /\ (cx=0 \/ cx=1))
Observation mpsc-rb+1p1c+bounded Always 3 0
Time mpsc-rb+1p1c+bounded 0.03
Hash=5bdad0f41557a641370e7fa6b8eb2f43
/* 2p1c bounded case */
$ herd7 -unroll 0 -conf linux-kernel.cfg litmus-tests/mpsc-rb+2p1c+bounded.litmus
Test mpsc-rb+2p1c+bounded Allowed
States 4
0:rFail=0; 1:rFail=0; 2:rFail=0; cx=0; dropped=1; len1=1; px=1;
0:rFail=0; 1:rFail=0; 2:rFail=0; cx=1; dropped=0; len1=1; px=2;
0:rFail=0; 1:rFail=0; 2:rFail=0; cx=1; dropped=1; len1=1; px=1;
0:rFail=0; 1:rFail=0; 2:rFail=0; cx=2; dropped=0; len1=1; px=2;
Ok
Witnesses
Positive: 22 Negative: 0
Condition exists (0:rFail=0 /\ 1:rFail=0 /\ 2:rFail=0 /\ len1=1 /\ (dropped=0 /\ px=2 /\ (cx=1 \/ cx=2) \/ dropped=1 /\ px=1 /\ (cx=0 \/ cx=1)))
Observation mpsc-rb+2p1c+bounded Always 22 0
Time mpsc-rb+2p1c+bounded 119.38
Hash=e2f8f442a02bf7d8c2988ba82cf002d2
/* 1p1c unbounded case */
$ herd7 -unroll 0 -conf linux-kernel.cfg litmus-tests/mpsc-rb+1p1c+unbound.litmus
Test mpsc-rb+1p1c+unbound Allowed
States 2
0:rFail=0; 1:rFail=0; cx=0; len1=1; px=1;
0:rFail=0; 1:rFail=0; cx=1; len1=1; px=1;
Ok
Witnesses
Positive: 3 Negative: 0
Condition exists (0:rFail=0 /\ 1:rFail=0 /\ px=1 /\ len1=1 /\ (cx=0 \/ cx=1))
Observation mpsc-rb+1p1c+unbound Always 3 0
Time mpsc-rb+1p1c+unbound 0.02
Hash=be9de6487d8e27c3d37802d122e4a87c
/* 2p1c unbounded case */
$ herd7 -unroll 0 -conf linux-kernel.cfg litmus-tests/mpsc-rb+2p1c+unbound.litmus
Test mpsc-rb+2p1c+unbound Allowed
States 3
0:rFail=0; 1:rFail=0; 2:rFail=0; cx=0; len1=1; len2=1; px=2;
0:rFail=0; 1:rFail=0; 2:rFail=0; cx=1; len1=1; len2=1; px=2;
0:rFail=0; 1:rFail=0; 2:rFail=0; cx=2; len1=1; len2=1; px=2;
Ok
Witnesses
Positive: 42 Negative: 0
Condition exists (0:rFail=0 /\ 1:rFail=0 /\ 2:rFail=0 /\ px=2 /\ len1=1 /\ len2=1 /\ (cx=0 \/ cx=1 \/ cx=2))
Observation mpsc-rb+2p1c+unbound Always 42 0
Time mpsc-rb+2p1c+unbound 39.19
Hash=f0352aba9bdc03dd0b1def7d0c4956fa
/* 3p1c bounded case */
$ herd7 -unroll 0 -conf linux-kernel.cfg mpsc-rb+3p1c+bounded.litmus
Test mpsc+ringbuf-spinlock Allowed
States 5
0:rFail=0; 1:rFail=0; 2:rFail=0; 3:rFail=0; cx=0; len1=1; len2=1; px=2;
0:rFail=0; 1:rFail=0; 2:rFail=0; 3:rFail=0; cx=1; len1=1; len2=1; px=2;
0:rFail=0; 1:rFail=0; 2:rFail=0; 3:rFail=0; cx=1; len1=1; len2=1; px=3;
0:rFail=0; 1:rFail=0; 2:rFail=0; 3:rFail=0; cx=2; len1=1; len2=1; px=2;
0:rFail=0; 1:rFail=0; 2:rFail=0; 3:rFail=0; cx=2; len1=1; len2=1; px=3;
Ok
Witnesses
Positive: 558 Negative: 0
Condition exists (0:rFail=0 /\ 1:rFail=0 /\ 2:rFail=0 /\ 3:rFail=0 /\ len1=1 /\ len2=1 /\ (px=2 /\ (cx=0 \/ cx=1 \/ cx=2) \/ px=3 /\ (cx=1 \/ cx=2)))
Observation mpsc+ringbuf-spinlock Always 558 0
Time mpsc+ringbuf-spinlock 57487.24
Hash=133977dba930d167b4e1b4a6923d5687
Cc: Paul E. McKenney <paulmck@kernel.org>
Signed-off-by: Andrii Nakryiko <andriin@fb.com>
---
.../litmus-tests/mpsc-rb+1p1c+bounded.litmus | 92 +++++++++++
.../litmus-tests/mpsc-rb+1p1c+unbound.litmus | 83 ++++++++++
.../litmus-tests/mpsc-rb+2p1c+bounded.litmus | 152 ++++++++++++++++++
.../litmus-tests/mpsc-rb+2p1c+unbound.litmus | 137 ++++++++++++++++
4 files changed, 464 insertions(+)
create mode 100644 tools/memory-model/litmus-tests/mpsc-rb+1p1c+bounded.litmus
create mode 100644 tools/memory-model/litmus-tests/mpsc-rb+1p1c+unbound.litmus
create mode 100644 tools/memory-model/litmus-tests/mpsc-rb+2p1c+bounded.litmus
create mode 100644 tools/memory-model/litmus-tests/mpsc-rb+2p1c+unbound.litmus
diff --git a/tools/memory-model/litmus-tests/mpsc-rb+1p1c+bounded.litmus b/tools/memory-model/litmus-tests/mpsc-rb+1p1c+bounded.litmus
new file mode 100644
index 000000000000..cafd17afe11e
--- /dev/null
+++ b/tools/memory-model/litmus-tests/mpsc-rb+1p1c+bounded.litmus
@@ -0,0 +1,92 @@
+C mpsc-rb+1p1c+bounded
+
+(*
+ * Result: Always
+ *
+ * This litmus test validates BPF ring buffer implementation under the
+ * following assumptions:
+ * - 1 producer;
+ * - 1 consumer;
+ * - ring buffer has capacity for only 1 record.
+ *
+ * Expectations:
+ * - 1 record pushed into ring buffer;
+ * - 0 or 1 element is consumed.
+ * - no failures.
+ *)
+
+{
+ max_len = 1;
+ len1 = 0;
+ px = 0;
+ cx = 0;
+ dropped = 0;
+}
+
+P0(int *len1, int *cx, int *px)
+{
+ int *rLenPtr;
+ int rLen;
+ int rPx;
+ int rCx;
+ int rFail;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ rPx = smp_load_acquire(px);
+ if (rCx < rPx) {
+ if (rCx == 0)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ rLen = smp_load_acquire(rLenPtr);
+ if (rLen == 0) {
+ rFail = 1;
+ } else if (rLen == 1) {
+ rCx = rCx + 1;
+ smp_store_release(cx, rCx);
+ }
+ }
+}
+
+P1(int *len1, spinlock_t *rb_lock, int *px, int *cx, int *dropped, int *max_len)
+{
+ int rPx;
+ int rCx;
+ int rFail;
+ int *rLenPtr;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ spin_lock(rb_lock);
+
+ rPx = *px;
+ if (rPx - rCx >= *max_len) {
+ atomic_inc(dropped);
+ spin_unlock(rb_lock);
+ } else {
+ if (rPx == 0)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ *rLenPtr = -1;
+ smp_wmb();
+ smp_store_release(px, rPx + 1);
+
+ spin_unlock(rb_lock);
+
+ smp_store_release(rLenPtr, 1);
+ }
+}
+
+exists (
+ 0:rFail=0 /\ 1:rFail=0
+ /\
+ (
+ (dropped=0 /\ px=1 /\ len1=1 /\ (cx=0 \/ cx=1))
+ )
+)
diff --git a/tools/memory-model/litmus-tests/mpsc-rb+1p1c+unbound.litmus b/tools/memory-model/litmus-tests/mpsc-rb+1p1c+unbound.litmus
new file mode 100644
index 000000000000..84f660598015
--- /dev/null
+++ b/tools/memory-model/litmus-tests/mpsc-rb+1p1c+unbound.litmus
@@ -0,0 +1,83 @@
+C mpsc-rb+1p1c+unbound
+
+(*
+ * Result: Always
+ *
+ * This litmus test validates BPF ring buffer implementation under the
+ * following assumptions:
+ * - 1 producer;
+ * - 1 consumer;
+ * - ring buffer capacity is unbounded.
+ *
+ * Expectations:
+ * - 1 record pushed into ring buffer;
+ * - 0 or 1 element is consumed.
+ * - no failures.
+ *)
+
+{
+ len1 = 0;
+ px = 0;
+ cx = 0;
+}
+
+P0(int *len1, int *cx, int *px)
+{
+ int *rLenPtr;
+ int rLen;
+ int rPx;
+ int rCx;
+ int rFail;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ rPx = smp_load_acquire(px);
+ if (rCx < rPx) {
+ if (rCx == 0)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ rLen = smp_load_acquire(rLenPtr);
+ if (rLen == 0) {
+ rFail = 1;
+ } else if (rLen == 1) {
+ rCx = rCx + 1;
+ smp_store_release(cx, rCx);
+ }
+ }
+}
+
+P1(int *len1, spinlock_t *rb_lock, int *px, int *cx)
+{
+ int rPx;
+ int rCx;
+ int rFail;
+ int *rLenPtr;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ spin_lock(rb_lock);
+
+ rPx = *px;
+ if (rPx == 0)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ *rLenPtr = -1;
+ smp_wmb();
+ smp_store_release(px, rPx + 1);
+
+ spin_unlock(rb_lock);
+
+ smp_store_release(rLenPtr, 1);
+}
+
+exists (
+ 0:rFail=0 /\ 1:rFail=0
+ /\ px=1 /\ len1=1
+ /\ (cx=0 \/ cx=1)
+)
diff --git a/tools/memory-model/litmus-tests/mpsc-rb+2p1c+bounded.litmus b/tools/memory-model/litmus-tests/mpsc-rb+2p1c+bounded.litmus
new file mode 100644
index 000000000000..900104c4933b
--- /dev/null
+++ b/tools/memory-model/litmus-tests/mpsc-rb+2p1c+bounded.litmus
@@ -0,0 +1,152 @@
+C mpsc-rb+2p1c+bounded
+
+(*
+ * Result: Always
+ *
+ * This litmus test validates BPF ring buffer implementation under the
+ * following assumptions:
+ * - 2 identical producers;
+ * - 1 consumer;
+ * - ring buffer has capacity for only 1 record.
+ *
+ * Expectations:
+ * - either 1 or 2 records are pushed into ring buffer;
+ * - 0, 1, or 2 elements are consumed by consumer;
+ * - appropriate number of dropped records is recorded to satisfy ring buffer
+ * size bounds;
+ * - no failures.
+ *)
+
+{
+ max_len = 1;
+ len1 = 0;
+ px = 0;
+ cx = 0;
+ dropped = 0;
+}
+
+P0(int *len1, int *cx, int *px)
+{
+ int *rLenPtr;
+ int rLen;
+ int rPx;
+ int rCx;
+ int rFail;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ rPx = smp_load_acquire(px);
+ if (rCx < rPx) {
+ if (rCx == 0)
+ rLenPtr = len1;
+ else if (rCx == 1)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ rLen = smp_load_acquire(rLenPtr);
+ if (rLen == 0) {
+ rFail = 1;
+ } else if (rLen == 1) {
+ rCx = rCx + 1;
+ smp_store_release(cx, rCx);
+ }
+ }
+
+ rPx = smp_load_acquire(px);
+ if (rCx < rPx) {
+ if (rCx == 0)
+ rLenPtr = len1;
+ else if (rCx == 1)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ rLen = smp_load_acquire(rLenPtr);
+ if (rLen == 0) {
+ rFail = 1;
+ } else if (rLen == 1) {
+ rCx = rCx + 1;
+ smp_store_release(cx, rCx);
+ }
+ }
+}
+
+P1(int *len1, spinlock_t *rb_lock, int *px, int *cx, int *dropped, int *max_len)
+{
+ int rPx;
+ int rCx;
+ int rFail;
+ int *rLenPtr;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ spin_lock(rb_lock);
+
+ rPx = *px;
+ if (rPx - rCx >= *max_len) {
+ atomic_inc(dropped);
+ spin_unlock(rb_lock);
+ } else {
+ if (rPx == 0)
+ rLenPtr = len1;
+ else if (rPx == 1)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ *rLenPtr = -1;
+ smp_wmb();
+ smp_store_release(px, rPx + 1);
+
+ spin_unlock(rb_lock);
+
+ smp_store_release(rLenPtr, 1);
+ }
+}
+
+P2(int *len1, spinlock_t *rb_lock, int *px, int *cx, int *dropped, int *max_len)
+{
+ int rPx;
+ int rCx;
+ int rFail;
+ int *rLenPtr;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ spin_lock(rb_lock);
+
+ rPx = *px;
+ if (rPx - rCx >= *max_len) {
+ atomic_inc(dropped);
+ spin_unlock(rb_lock);
+ } else {
+ if (rPx == 0)
+ rLenPtr = len1;
+ else if (rPx == 1)
+ rLenPtr = len1;
+ else
+ rFail = 1;
+
+ *rLenPtr = -1;
+ smp_wmb();
+ smp_store_release(px, rPx + 1);
+
+ spin_unlock(rb_lock);
+
+ smp_store_release(rLenPtr, 1);
+ }
+}
+
+exists (
+ 0:rFail=0 /\ 1:rFail=0 /\ 2:rFail=0 /\ len1=1
+ /\
+ (
+ (dropped = 0 /\ px=2 /\ (cx=1 \/ cx=2))
+ \/
+ (dropped = 1 /\ px=1 /\ (cx=0 \/ cx=1))
+ )
+)
diff --git a/tools/memory-model/litmus-tests/mpsc-rb+2p1c+unbound.litmus b/tools/memory-model/litmus-tests/mpsc-rb+2p1c+unbound.litmus
new file mode 100644
index 000000000000..83372e9eb079
--- /dev/null
+++ b/tools/memory-model/litmus-tests/mpsc-rb+2p1c+unbound.litmus
@@ -0,0 +1,137 @@
+C mpsc-rb+2p1c+unbound
+
+(*
+ * Result: Always
+ *
+ * This litmus test validates BPF ring buffer implementation under the
+ * following assumptions:
+ * - 2 identical producers;
+ * - 1 consumer;
+ * - ring buffer capacity is unbounded.
+ *
+ * Expectations:
+ * - 2 records pushed into ring buffer;
+ * - 0, 1, or 2 elements are consumed.
+ * - no failures.
+ *)
+
+{
+ len1 = 0;
+ len2 = 0;
+ px = 0;
+ cx = 0;
+}
+
+P0(int *len1, int *len2, int *cx, int *px)
+{
+ int *rLenPtr;
+ int rLen;
+ int rPx;
+ int rCx;
+ int rFail;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ rPx = smp_load_acquire(px);
+ if (rCx < rPx) {
+ if (rCx == 0)
+ rLenPtr = len1;
+ else if (rCx == 1)
+ rLenPtr = len2;
+ else
+ rFail = 1;
+
+ rLen = smp_load_acquire(rLenPtr);
+ if (rLen == 0) {
+ rFail = 1;
+ } else if (rLen == 1) {
+ rCx = rCx + 1;
+ smp_store_release(cx, rCx);
+ }
+ }
+
+ rPx = smp_load_acquire(px);
+ if (rCx < rPx) {
+ if (rCx == 0)
+ rLenPtr = len1;
+ else if (rCx == 1)
+ rLenPtr = len2;
+ else
+ rFail = 1;
+
+ rLen = smp_load_acquire(rLenPtr);
+ if (rLen == 0) {
+ rFail = 1;
+ } else if (rLen == 1) {
+ rCx = rCx + 1;
+ smp_store_release(cx, rCx);
+ }
+ }
+}
+
+P1(int *len1, int *len2, spinlock_t *rb_lock, int *px, int *cx)
+{
+ int rPx;
+ int rCx;
+ int rFail;
+ int *rLenPtr;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ spin_lock(rb_lock);
+
+ rPx = *px;
+ if (rPx == 0)
+ rLenPtr = len1;
+ else if (rPx == 1)
+ rLenPtr = len2;
+ else
+ rFail = 1;
+
+ *rLenPtr = -1;
+ smp_wmb();
+ smp_store_release(px, rPx + 1);
+
+ spin_unlock(rb_lock);
+
+ smp_store_release(rLenPtr, 1);
+}
+
+P2(int *len1, int *len2, spinlock_t *rb_lock, int *px, int *cx)
+{
+ int rPx;
+ int rCx;
+ int rFail;
+ int *rLenPtr;
+
+ rFail = 0;
+ rCx = smp_load_acquire(cx);
+
+ spin_lock(rb_lock);
+
+ rPx = *px;
+ if (rPx == 0)
+ rLenPtr = len1;
+ else if (rPx == 1)
+ rLenPtr = len2;
+ else
+ rFail = 1;
+
+ *rLenPtr = -1;
+ smp_wmb();
+ smp_store_release(px, rPx + 1);
+
+ spin_unlock(rb_lock);
+
+ smp_store_release(rLenPtr, 1);
+}
+
+exists (
+ 0:rFail=0 /\ 1:rFail=0 /\ 2:rFail=0
+ /\
+ px=2 /\ len1=1 /\ len2=1
+ /\
+ (cx=0 \/ cx=1 \/ cx=2)
+)
--
2.24.1
next prev parent reply other threads:[~2020-05-17 19:57 UTC|newest]
Thread overview: 33+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-05-17 19:57 [PATCH v2 bpf-next 0/7] BPF ring buffer Andrii Nakryiko
2020-05-17 19:57 ` [PATCH v2 bpf-next 1/7] bpf: implement BPF ring buffer and verifier support for it Andrii Nakryiko
2020-05-19 12:57 ` kbuild test robot
2020-05-19 23:53 ` kbuild test robot
2020-05-22 0:25 ` Paul E. McKenney
2020-05-22 18:46 ` Andrii Nakryiko
2020-05-25 16:01 ` Paul E. McKenney
2020-05-25 18:45 ` Andrii Nakryiko
2020-05-22 1:07 ` Alexei Starovoitov
2020-05-22 18:48 ` Andrii Nakryiko
2020-05-25 20:34 ` Andrii Nakryiko
2020-05-17 19:57 ` Andrii Nakryiko [this message]
2020-05-22 0:34 ` [PATCH v2 bpf-next 2/7] tools/memory-model: add BPF ringbuf MPSC litmus tests Paul E. McKenney
2020-05-22 18:51 ` Andrii Nakryiko
2020-05-25 23:33 ` Andrii Nakryiko
2020-05-26 3:05 ` Paul E. McKenney
2020-05-17 19:57 ` [PATCH v2 bpf-next 3/7] bpf: track reference type in verifier Andrii Nakryiko
2020-05-22 1:13 ` Alexei Starovoitov
2020-05-22 18:53 ` Andrii Nakryiko
2020-05-17 19:57 ` [PATCH v2 bpf-next 4/7] libbpf: add BPF ring buffer support Andrii Nakryiko
2020-05-22 1:15 ` Alexei Starovoitov
2020-05-22 18:56 ` Andrii Nakryiko
2020-05-17 19:57 ` [PATCH v2 bpf-next 5/7] selftests/bpf: add BPF ringbuf selftests Andrii Nakryiko
2020-05-22 1:20 ` Alexei Starovoitov
2020-05-22 18:58 ` Andrii Nakryiko
2020-05-17 19:57 ` [PATCH v2 bpf-next 6/7] bpf: add BPF ringbuf and perf buffer benchmarks Andrii Nakryiko
2020-05-22 1:21 ` Alexei Starovoitov
2020-05-22 19:07 ` Andrii Nakryiko
2020-05-17 19:57 ` [PATCH v2 bpf-next 7/7] docs/bpf: add BPF ring buffer design notes Andrii Nakryiko
2020-05-22 1:23 ` Alexei Starovoitov
2020-05-22 19:08 ` Andrii Nakryiko
2020-05-25 9:59 ` Alban Crequy
2020-05-25 19:12 ` Andrii Nakryiko
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-all 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=20200517195727.279322-3-andriin@fb.com \
--to=andriin@fb.com \
--cc=andrii.nakryiko@gmail.com \
--cc=ast@fb.com \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=jonathan.lemon@gmail.com \
--cc=kernel-team@fb.com \
--cc=netdev@vger.kernel.org \
--cc=paulmck@kernel.org \
/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;
as well as URLs for NNTP newsgroup(s).