From 8b0ab33247e7ac86f2cecd144991301b6fe6a55b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 7 Aug 2026 08:18:03 +0200 Subject: [PATCH 1/5] compat/posix: introduce writev(3p) wrapper In a subsequent commit we're going to add the first caller to writev(3p). Introduce a compatibility wrapper for this syscall that we can use on systems that don't have this syscall. The syscall exists on modern Unixes like Linux and macOS, and seemingly even for NonStop according to [1]. It doesn't seem to exist on Windows though. [1]: http://nonstoptools.com/manuals/OSS-SystemCalls.pdf [2]: https://www.gnu.org/software/gnulib/manual/html_node/writev.html Helped-by: Johannes Schindelin Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Makefile | 4 +++ compat/posix.h | 14 ++++++++++ compat/writev.c | 41 +++++++++++++++++++++++++++++ config.mak.uname | 2 ++ contrib/buildsystems/CMakeLists.txt | 6 ++++- meson.build | 1 + 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 compat/writev.c diff --git a/Makefile b/Makefile index 1f3f099f5c..eda5ecc5b4 100644 --- a/Makefile +++ b/Makefile @@ -2033,6 +2033,10 @@ ifdef NO_PREAD COMPAT_CFLAGS += -DNO_PREAD COMPAT_OBJS += compat/pread.o endif +ifdef NO_WRITEV + COMPAT_CFLAGS += -DNO_WRITEV + COMPAT_OBJS += compat/writev.o +endif ifdef NO_FAST_WORKING_DIRECTORY BASIC_CFLAGS += -DNO_FAST_WORKING_DIRECTORY endif diff --git a/compat/posix.h b/compat/posix.h index e2e794cad7..71cc731620 100644 --- a/compat/posix.h +++ b/compat/posix.h @@ -148,6 +148,9 @@ #include #include #include +#ifndef NO_WRITEV +#include +#endif #include #ifndef NO_SYS_SELECT_H #include @@ -334,6 +337,17 @@ int git_lstat(const char *, struct stat *); ssize_t git_pread(int fd, void *buf, size_t count, off_t offset); #endif +#ifdef NO_WRITEV +#define writev git_writev +#define iovec git_iovec +struct git_iovec { + void *iov_base; + size_t iov_len; +}; + +ssize_t git_writev(int fd, const struct iovec *iov, int iovcnt); +#endif + #ifdef NO_SETENV #define setenv gitsetenv int gitsetenv(const char *, const char *, int); diff --git a/compat/writev.c b/compat/writev.c new file mode 100644 index 0000000000..540f66de61 --- /dev/null +++ b/compat/writev.c @@ -0,0 +1,41 @@ +#include "../git-compat-util.h" +#include "../wrapper.h" + +ssize_t git_writev(int fd, const struct iovec *iov, int iovcnt) +{ + size_t sum = 0; + + if (iovcnt <= 0) { + errno = EINVAL; + return -1; + } + + /* + * According to writev(3p), the syscall shall error with EINVAL in case + * the sum of `iov_len` overflows `ssize_t`. + */ + for (int i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > maximum_signed_value_of_type(ssize_t) || + unsigned_add_overflows(iov[i].iov_len, sum) || + iov[i].iov_len + sum > maximum_signed_value_of_type(ssize_t)) { + errno = EINVAL; + return -1; + } + + sum += iov[i].iov_len; + } + + /* + * We only ever write the first non-empty vector so that we can + * guarantee the call to be non-interleaving as guaranteed by POSIX. + * This works just fine as callers have to loop around writev anyway. + */ + for (int i = 0; i < iovcnt; i++) { + if (!iov[i].iov_len) + continue; + return xwrite(fd, iov[i].iov_base, iov[i].iov_len); + } + + /* When all iovec members were zero we ought to return 0 according to POSIX. */ + return 0; +} diff --git a/config.mak.uname b/config.mak.uname index 9ebd240378..95ef6e64dc 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -483,6 +483,7 @@ ifeq ($(uname_S),Windows) SANE_TOOL_PATH ?= $(msvc_bin_dir_msys) HAVE_ALLOCA_H = YesPlease NO_PREAD = YesPlease + NO_WRITEV = YesPlease NEEDS_CRYPTO_WITH_SSL = YesPlease NO_LIBGEN_H = YesPlease NO_POLL = YesPlease @@ -697,6 +698,7 @@ ifeq ($(uname_S),MINGW) pathsep = ; HAVE_ALLOCA_H = YesPlease NO_PREAD = YesPlease + NO_WRITEV = YesPlease NEEDS_CRYPTO_WITH_SSL = YesPlease NO_LIBGEN_H = YesPlease NO_POLL = YesPlease diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index a57c4b464f..8f56203f34 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -378,7 +378,7 @@ endif() #function checks set(function_checks strcasestr memmem strlcpy strtoimax strtoumax strtoull - setenv mkdtemp poll pread memmem) + setenv mkdtemp poll pread memmem writev) #unsetenv,hstrerror are incompatible with windows build if(NOT WIN32) @@ -423,6 +423,10 @@ if(NOT HAVE_MEMMEM) list(APPEND compat_SOURCES compat/memmem.c) endif() +if(NOT HAVE_WRITEV) + list(APPEND compat_SOURCES compat/writev.c) +endif() + if(NOT WIN32) if(NOT HAVE_UNSETENV) list(APPEND compat_SOURCES compat/unsetenv.c) diff --git a/meson.build b/meson.build index 9434b56960..43373924aa 100644 --- a/meson.build +++ b/meson.build @@ -1448,6 +1448,7 @@ checkfuncs = { 'initgroups' : [], 'strtoumax' : ['strtoumax.c', 'strtoimax.c'], 'pread' : ['pread.c'], + 'writev' : ['writev.c'], } if host_machine.system() == 'windows' From d70eb7f3600db5fabb538ce35b186db68854b2a8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 7 Aug 2026 08:18:04 +0200 Subject: [PATCH 2/5] wrapper: introduce writev(3p) wrappers In the preceding commit we have added a compatibility wrapper for the writev(3p) syscall. Introduce some generic wrappers for this function that we nowadays take for granted in the Git codebase. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- wrapper.c | 41 +++++++++++++++++++++++++++++++++++++++++ wrapper.h | 9 +++++++++ write-or-die.c | 8 ++++++++ write-or-die.h | 1 + 4 files changed, 59 insertions(+) diff --git a/wrapper.c b/wrapper.c index 16f5a63fbb..be8fa575e6 100644 --- a/wrapper.c +++ b/wrapper.c @@ -323,6 +323,47 @@ ssize_t write_in_full(int fd, const void *buf, size_t count) return total; } +ssize_t writev_in_full(int fd, struct iovec *iov, int iovcnt) +{ + ssize_t total_written = 0; + + while (iovcnt) { + ssize_t bytes_written = writev(fd, iov, iovcnt); + if (bytes_written < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + return -1; + } + if (!bytes_written) { + errno = ENOSPC; + return -1; + } + + total_written += bytes_written; + + /* + * We first need to discard any iovec entities that have been + * fully written. + */ + while (iovcnt && (size_t)bytes_written >= iov->iov_len) { + bytes_written -= iov->iov_len; + iov++; + iovcnt--; + } + + /* + * Finally, we need to adjust the last iovec in case we have + * performed a partial write. + */ + if (iovcnt && bytes_written) { + iov->iov_base = (char *) iov->iov_base + bytes_written; + iov->iov_len -= bytes_written; + } + } + + return total_written; +} + ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset) { char *p = buf; diff --git a/wrapper.h b/wrapper.h index 15ac3bab6e..27519b32d1 100644 --- a/wrapper.h +++ b/wrapper.h @@ -47,6 +47,15 @@ ssize_t read_in_full(int fd, void *buf, size_t count); ssize_t write_in_full(int fd, const void *buf, size_t count); ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset); +/* + * Try to write all iovecs. Returns -1 in case an error occurred with a proper + * errno set, the number of bytes written otherwise. + * + * Note that the iovec will be modified as a result of this call to adjust for + * partial writes! + */ +ssize_t writev_in_full(int fd, struct iovec *iov, int iovcnt); + static inline ssize_t write_str_in_full(int fd, const char *str) { return write_in_full(fd, str, strlen(str)); diff --git a/write-or-die.c b/write-or-die.c index 01a9a51fa2..5f522fb728 100644 --- a/write-or-die.c +++ b/write-or-die.c @@ -96,6 +96,14 @@ void write_or_die(int fd, const void *buf, size_t count) } } +void writev_or_die(int fd, struct iovec *iov, int iovlen) +{ + if (writev_in_full(fd, iov, iovlen) < 0) { + check_pipe(errno); + die_errno("writev error"); + } +} + void fwrite_or_die(FILE *f, const void *buf, size_t count) { if (fwrite(buf, 1, count, f) != count) diff --git a/write-or-die.h b/write-or-die.h index ff0408bd84..a045bdfaef 100644 --- a/write-or-die.h +++ b/write-or-die.h @@ -7,6 +7,7 @@ void fprintf_or_die(FILE *, const char *fmt, ...); void fwrite_or_die(FILE *f, const void *buf, size_t count); void fflush_or_die(FILE *f); void write_or_die(int fd, const void *buf, size_t count); +void writev_or_die(int fd, struct iovec *iov, int iovlen); /* * These values are used to help identify parts of a repository to fsync. From a4e2c0fc81198fa84c1107daa0a33de6cc6d9c3a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 7 Aug 2026 08:18:05 +0200 Subject: [PATCH 3/5] wrapper: properly handle MAX_IO_SIZE in writev(3p) Some systems like NonStop set a comparatively small `MAX_IO_SIZE`, which limits the maximum number of bytes we're allowed to write in a single call. We already handle this limit properly in `xwrite()`, but we have recently introduced wrappers for writev(3p) where we don't. This will cause the syscall to return EINVAL in case somebody passes an iovec entry to writev(3p) that is larger than `MAX_IO_SIZE`. Introduce a new function `xwritev()` that is similar to `xwrite()` in that it handles such platform-specific nuances: - We only pass the leading iovec entries to writev(3p) that fit into `MAX_IO_SIZE`, pretending that the underlying syscall performed a short write. This mirrors how `xwrite()` chomps overly large requests before handing them to write(3p). As a consequence, callers will never see writev(3p)'s EINVAL error for requests whose summed length would overflow an ssize_t, but observe a short write instead. - If already the first iovec entry exceeds the limit we instead punt to `xwrite()`, which knows to handle this case for us. - We restart the underlying syscall on EINTR and EAGAIN, just like `xwrite()` does for write(3p). Adapt `writev_in_full()` to use this new wrapper. With the retry logic now living in `xwritev()`, the calling loop becomes the exact mirror image of `write_in_full()`, which also retains the responsibility of translating a zero-length write into ENOSPC. Reported-by: Randall Becker Helped-by: Jeff King Helped-by: Junio C Hamano Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- wrapper.c | 47 ++++++++++++++++++++++++++++++++++++++++++----- wrapper.h | 1 + 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/wrapper.c b/wrapper.c index be8fa575e6..561f9ee9c9 100644 --- a/wrapper.c +++ b/wrapper.c @@ -323,17 +323,54 @@ ssize_t write_in_full(int fd, const void *buf, size_t count) return total; } +ssize_t xwritev(int fd, struct iovec *iov, int iovcnt) +{ + size_t allowed = MAX_IO_SIZE; + int i; + + /* + * Some platforms define a comparatively small `MAX_IO_SIZE` that + * limits how many bytes can be written with a single call to + * write(3p) or writev(3p); exceeding that limit causes the syscall to + * fail with EINVAL. Just like xwrite() chomps overly large requests + * for write(3p), pretend that the underlying writev(3p) performed a + * short write by only passing along the leading iovec entries that + * fit into that limit. + */ + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > allowed) { + /* + * If the first buffer is larger than MAX_IO_SIZE, + * let xwrite() deal with it. + */ + if (!i) + return xwrite(fd, iov->iov_base, iov->iov_len); + break; + } + allowed -= iov[i].iov_len; + } + + while (1) { + ssize_t bytes_written = writev(fd, iov, i); + if (bytes_written < 0) { + if (errno == EINTR) + continue; + if (handle_nonblock(fd, POLLOUT, errno)) + continue; + } + + return bytes_written; + } +} + ssize_t writev_in_full(int fd, struct iovec *iov, int iovcnt) { ssize_t total_written = 0; while (iovcnt) { - ssize_t bytes_written = writev(fd, iov, iovcnt); - if (bytes_written < 0) { - if (errno == EINTR || errno == EAGAIN) - continue; + ssize_t bytes_written = xwritev(fd, iov, iovcnt); + if (bytes_written < 0) return -1; - } if (!bytes_written) { errno = ENOSPC; return -1; diff --git a/wrapper.h b/wrapper.h index 27519b32d1..a6287d7f4d 100644 --- a/wrapper.h +++ b/wrapper.h @@ -16,6 +16,7 @@ void *xmmap_gently(void *start, size_t length, int prot, int flags, int fd, off_ int xopen(const char *path, int flags, ...); ssize_t xread(int fd, void *buf, size_t len); ssize_t xwrite(int fd, const void *buf, size_t len); +ssize_t xwritev(int fd, struct iovec *iov, int iovcnt); ssize_t xpread(int fd, void *buf, size_t len, off_t offset); int xdup(int fd); FILE *xfopen(const char *path, const char *mode); From 21db416cd2bf658ce79fc928c65b86e981062e8e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 7 Aug 2026 08:18:06 +0200 Subject: [PATCH 4/5] sideband: use writev(3p) to send pktlines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every pktline that we send out via `send_sideband()` currently requires two syscalls: one to write the pktline's length, and one to send its data. This typically isn't all that much of a problem, but under extreme load the syscalls may cause contention in the kernel. Refactor the code to instead use the newly introduced writev(3p) infra so that we can send out the data with a single syscall. This reduces the number of syscalls from around 133,000 calls to write(3p) to around 67,000 calls to writev(3p). This change leads to a performance improvement for git-upload-pack(1), but we have to cheat a bit to really make it measurable. Usually, the time is strongly dominated by generating the packfile itself. But if we precompute the pack and serve it via the pack-objects hook then we can essentially eliminate that overhead. The following setup is executed in the Git repository: $ cat >request <<-EOF 0048want 5ce91c059e41090e7d2cffad39c04af8acf98dc1 side-band no-progress 00000009done EOF $ echo 5ce91c059e41090e7d2cffad39c04af8acf98dc1 | git pack-objects --revs --stdout >pack $ cat >hook <<-EOF #!/bin/sh cat >/dev/null cat "$(pwd)"/pack EOF $ chmod u+x hook $ git -c uploadpack.packObjectsHook="$(pwd)"/hook upload-pack . Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- sideband.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/sideband.c b/sideband.c index 1523a53e1d..94e5b56172 100644 --- a/sideband.c +++ b/sideband.c @@ -441,6 +441,7 @@ void send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_ma const char *p = data; while (sz) { + struct iovec iov[2]; unsigned n; char hdr[5]; @@ -450,12 +451,19 @@ void send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_ma if (0 <= band) { xsnprintf(hdr, sizeof(hdr), "%04x", n + 5); hdr[4] = band; - write_or_die(fd, hdr, 5); + iov[0].iov_base = hdr; + iov[0].iov_len = 5; } else { xsnprintf(hdr, sizeof(hdr), "%04x", n + 4); - write_or_die(fd, hdr, 4); + iov[0].iov_base = hdr; + iov[0].iov_len = 4; } - write_or_die(fd, p, n); + + iov[1].iov_base = (void *) p; + iov[1].iov_len = n; + + writev_or_die(fd, iov, ARRAY_SIZE(iov)); + p += n; sz -= n; } From 5bd4f43456aae6fa942eb6c6ace6244d09e01d08 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 7 Aug 2026 08:18:07 +0200 Subject: [PATCH 5/5] fast-import: use writev(3p) to send cat-blob responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When answering a `cat-blob` command, `cat_blob()` issues three separate calls to write(3p) on the cat-blob fd: one for the header line, one for the full blob payload, and one for the trailing newline. Frontends like git-filter-repo issue these commands in bulk, once per rewritten blob, so the syscall overhead adds up. Use `writev_in_full()` to send all three parts with a single syscall. This can be benchmarked with the following setup: $ git cat-file --unordered --filter=object:type=blob --batch-check='cat-blob %(objectname)' --batch-all-objects >request $ git fast-import --cat-blob-fd=3 Signed-off-by: Junio C Hamano --- builtin/fast-import.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/builtin/fast-import.c b/builtin/fast-import.c index aa656c5195..48fda01c94 100644 --- a/builtin/fast-import.c +++ b/builtin/fast-import.c @@ -3332,6 +3332,7 @@ static void cat_blob_write(const char *buf, unsigned long size) static void cat_blob(struct object_entry *oe, struct object_id *oid) { struct strbuf line = STRBUF_INIT; + struct iovec iov[3]; unsigned long size; enum object_type type = 0; char *buf; @@ -3365,10 +3366,21 @@ static void cat_blob(struct object_entry *oe, struct object_id *oid) strbuf_reset(&line); strbuf_addf(&line, "%s %s %"PRIuMAX"\n", oid_to_hex(oid), type_name(type), (uintmax_t)size); - cat_blob_write(line.buf, line.len); + + /* + * Write the header, the payload and the trailing newline with a + * single writev(3p) call instead of three separate write(3p) calls. + */ + iov[0].iov_base = line.buf; + iov[0].iov_len = line.len; + iov[1].iov_base = buf; + iov[1].iov_len = size; + iov[2].iov_base = (void *) "\n"; + iov[2].iov_len = 1; + + if (writev_in_full(cat_blob_fd, iov, ARRAY_SIZE(iov)) < 0) + die_errno(_("write to frontend failed")); strbuf_release(&line); - cat_blob_write(buf, size); - cat_blob_write("\n", 1); if (oe && oe->pack_id == pack_id) { last_blob.offset = oe->idx.offset; strbuf_attach(&last_blob.data, buf, size, size + 1);