X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FSpawn.pm;h=1ee40503c33f0b014bb6814295d890e3335133e2;hb=af0b0fb7a454470a32c452119d0392e0dedb3fe1;hp=6d42d5bc70c39318865848013d2f7e96b77acbf8;hpb=267371b1273b518215939e817e53733584b68af7;p=public-inbox.git diff --git a/lib/PublicInbox/Spawn.pm b/lib/PublicInbox/Spawn.pm index 6d42d5bc..1ee40503 100644 --- a/lib/PublicInbox/Spawn.pm +++ b/lib/PublicInbox/Spawn.pm @@ -1,32 +1,34 @@ -# Copyright (C) 2016-2019 all contributors +# Copyright (C) 2016-2021 all contributors # License: AGPL-3.0+ # # This allows vfork to be used for spawning subprocesses if -# PERL_INLINE_DIRECTORY is explicitly defined in the environment. +# ~/.cache/public-inbox/inline-c is writable or if PERL_INLINE_DIRECTORY +# is explicitly defined in the environment (and writable). # Under Linux, vfork can make a big difference in spawning performance # as process size increases (fork still needs to mark pages for CoW use). # Currently, we only use this for code intended for long running # daemons (inside the PSGI code (-httpd) and -nntpd). The short-lived # scripts (-mda, -index, -learn, -init) either use IPC::run or standard # Perl routines. +# +# There'll probably be more OS-level C stuff here, down the line. +# We don't want too many DSOs: https://udrepper.livejournal.com/8790.html package PublicInbox::Spawn; use strict; -use warnings; -use base qw(Exporter); +use parent qw(Exporter); use Symbol qw(gensym); -use IO::Handle; use PublicInbox::ProcessPipe; -our @EXPORT_OK = qw/which spawn popen_rd/; -sub RLIMITS () { qw(RLIMIT_CPU RLIMIT_CORE RLIMIT_DATA) } +our @EXPORT_OK = qw(which spawn popen_rd run_die nodatacow_dir); +our @RLIMITS = qw(RLIMIT_CPU RLIMIT_CORE RLIMIT_DATA); my $vfork_spawn = <<'VFORK_SPAWN'; #include -#include #include #include #include #include +#include /* some platforms need alloca.h, but some don't */ #if defined(__GNUC__) && !defined(alloca) @@ -56,26 +58,10 @@ my $vfork_spawn = <<'VFORK_SPAWN'; dst[real_len] = 0; \ } while (0) -static void *deconst(const char *s) -{ - union { const char *in; void *out; } u; - u.in = s; - return u.out; -} - /* needs to be safe inside a vfork'ed process */ -static void xerr(const char *msg) +static void exit_err(int *cerrnum) { - struct iovec iov[3]; - const char *err = strerror(errno); /* should be safe in practice */ - - iov[0].iov_base = deconst(msg); - iov[0].iov_len = strlen(msg); - iov[1].iov_base = deconst(err); - iov[1].iov_len = strlen(err); - iov[2].iov_base = deconst("\n"); - iov[2].iov_len = 1; - writev(2, iov, 3); + *cerrnum = errno; _exit(1); } @@ -84,7 +70,8 @@ static void xerr(const char *msg) * whatever we'll need in the future. * Be sure to update PublicInbox::SpawnPP if this changes */ -int pi_fork_exec(SV *redirref, SV *file, SV *cmdref, SV *envref, SV *rlimref) +int pi_fork_exec(SV *redirref, SV *file, SV *cmdref, SV *envref, SV *rlimref, + const char *cd) { AV *redir = (AV *)SvRV(redirref); AV *cmd = (AV *)SvRV(cmdref); @@ -93,8 +80,8 @@ int pi_fork_exec(SV *redirref, SV *file, SV *cmdref, SV *envref, SV *rlimref) const char *filename = SvPV_nolen(file); pid_t pid; char **argv, **envp; - sigset_t set, old; - int ret, errnum; + sigset_t set, old, cset; + int ret, perrnum, cerrnum = 0; AV2C_COPY(argv, cmd); AV2C_COPY(envp, env); @@ -103,6 +90,10 @@ int pi_fork_exec(SV *redirref, SV *file, SV *cmdref, SV *envref, SV *rlimref) assert(ret == 0 && "BUG calling sigfillset"); ret = sigprocmask(SIG_SETMASK, &set, &old); assert(ret == 0 && "BUG calling sigprocmask to block"); + ret = sigemptyset(&cset); + assert(ret == 0 && "BUG calling sigemptyset"); + ret = sigaddset(&cset, SIGCHLD); + assert(ret == 0 && "BUG calling sigaddset for SIGCHLD"); pid = vfork(); if (pid == 0) { int sig; @@ -114,10 +105,12 @@ int pi_fork_exec(SV *redirref, SV *file, SV *cmdref, SV *envref, SV *rlimref) if (parent_fd == child_fd) continue; if (dup2(parent_fd, child_fd) < 0) - xerr("dup2"); + exit_err(&cerrnum); } for (sig = 1; sig < NSIG; sig++) signal(sig, SIG_DFL); /* ignore errors on signals */ + if (*cd && chdir(cd) < 0) + exit_err(&cerrnum); max = av_len(rlim); for (i = 0; i < max; i += 3) { @@ -129,27 +122,91 @@ int pi_fork_exec(SV *redirref, SV *file, SV *cmdref, SV *envref, SV *rlimref) rl.rlim_cur = SvIV(*soft); rl.rlim_max = SvIV(*hard); if (setrlimit(SvIV(*res), &rl) < 0) - xerr("sertlimit"); + exit_err(&cerrnum); } /* - * don't bother unblocking, we don't want signals - * to the group taking out a subprocess + * don't bother unblocking other signals for now, just SIGCHLD. + * we don't want signals to the group taking out a subprocess */ + (void)sigprocmask(SIG_UNBLOCK, &cset, NULL); execve(filename, argv, envp); - xerr("execve failed"); + exit_err(&cerrnum); } - errnum = errno; + perrnum = errno; ret = sigprocmask(SIG_SETMASK, &old, NULL); assert(ret == 0 && "BUG calling sigprocmask to restore"); - errno = errnum; - + if (cerrnum) { + if (pid > 0) + waitpid(pid, NULL, 0); + pid = -1; + errno = cerrnum; + } else if (perrnum) { + errno = perrnum; + } return (int)pid; } VFORK_SPAWN -my $inline_dir = $ENV{PERL_INLINE_DIRECTORY}; -$vfork_spawn = undef unless defined $inline_dir && -d $inline_dir && -w _; +# btrfs on Linux is copy-on-write (COW) by default. As of Linux 5.7, +# this still leads to fragmentation for SQLite and Xapian files where +# random I/O happens, so we disable COW just for SQLite files and Xapian +# directories. Disabling COW disables checksumming, so we only do this +# for regeneratable files, and not canonical git storage (git doesn't +# checksum refs, only data under $GIT_DIR/objects). +my $set_nodatacow = $^O eq 'linux' ? <<'SET_NODATACOW' : ''; +#include +#include +#include +#include +#include +#include +#include +#include + +void nodatacow_fd(int fd) +{ + struct statfs buf; + int val = 0; + + if (fstatfs(fd, &buf) < 0) { + fprintf(stderr, "fstatfs: %s\\n", strerror(errno)); + return; + } + + /* only btrfs is known to have this problem, so skip for non-btrfs */ + if (buf.f_type != BTRFS_SUPER_MAGIC) + return; + + if (ioctl(fd, FS_IOC_GETFLAGS, &val) < 0) { + fprintf(stderr, "FS_IOC_GET_FLAGS: %s\\n", strerror(errno)); + return; + } + val |= FS_NOCOW_FL; + if (ioctl(fd, FS_IOC_SETFLAGS, &val) < 0) + fprintf(stderr, "FS_IOC_SET_FLAGS: %s\\n", strerror(errno)); +} + +void nodatacow_dir(const char *dir) +{ + DIR *dh = opendir(dir); + int fd; + + if (!dh) croak("opendir(%s): %s", dir, strerror(errno)); + fd = dirfd(dh); + if (fd >= 0) + nodatacow_fd(fd); + /* ENOTSUP probably won't happen under Linux... */ + closedir(dh); +} +SET_NODATACOW + +my $inline_dir = $ENV{PERL_INLINE_DIRECTORY} //= ( + $ENV{XDG_CACHE_HOME} // + ( ($ENV{HOME} // '/nonexistent').'/.cache' ) + ).'/public-inbox/inline-c'; + +$set_nodatacow = $vfork_spawn = undef unless -d $inline_dir && -w _; if (defined $vfork_spawn) { # Inline 0.64 or later has locking in multi-process env, # but we support 0.5 on Debian wheezy @@ -158,14 +215,21 @@ if (defined $vfork_spawn) { my $f = "$inline_dir/.public-inbox.lock"; open my $fh, '>', $f or die "failed to open $f: $!\n"; flock($fh, LOCK_EX) or die "LOCK_EX failed on $f: $!\n"; - eval 'use Inline C => $vfork_spawn'; #, BUILD_NOISY => 1'; + eval 'use Inline C => $vfork_spawn . $set_nodatacow'; my $err = $@; + my $ndc_err; + if ($err && $set_nodatacow) { # missing Linux kernel headers + $ndc_err = $err; + undef $set_nodatacow; + eval 'use Inline C => $vfork_spawn'; + } flock($fh, LOCK_UN) or die "LOCK_UN failed on $f: $!\n"; die $err if $err; + warn $ndc_err if $ndc_err; }; if ($@) { warn "Inline::C failed for vfork: $@\n"; - $vfork_spawn = undef; + $set_nodatacow = $vfork_spawn = undef; } } @@ -173,6 +237,14 @@ unless (defined $vfork_spawn) { require PublicInbox::SpawnPP; *pi_fork_exec = \&PublicInbox::SpawnPP::pi_fork_exec } +unless ($set_nodatacow) { + require PublicInbox::NDC_PP; + no warnings 'once'; + *nodatacow_fd = \&PublicInbox::NDC_PP::nodatacow_fd; + *nodatacow_dir = \&PublicInbox::NDC_PP::nodatacow_dir; +} +undef $set_nodatacow; +undef $vfork_spawn; sub which ($) { my ($file) = @_; @@ -207,7 +279,7 @@ sub spawn ($;$$) { } my $rlim = []; - foreach my $l (RLIMITS()) { + foreach my $l (@RLIMITS) { defined(my $v = $opts->{$l}) or next; my $r = eval "require BSD::Resource; BSD::Resource::$l();"; unless (defined $r) { @@ -216,8 +288,10 @@ sub spawn ($;$$) { } push @$rlim, $r, @$v; } - my $pid = pi_fork_exec($redir, $f, $cmd, \@env, $rlim); - $pid < 0 ? undef : $pid; + my $cd = $opts->{'-C'} // ''; # undef => NULL mapping doesn't work? + my $pid = pi_fork_exec($redir, $f, $cmd, \@env, $rlim, $cd); + die "fork_exec @$cmd failed: $!\n" unless $pid > 0; + $pid; } sub popen_rd { @@ -226,11 +300,17 @@ sub popen_rd { $opts ||= {}; $opts->{1} = fileno($w); my $pid = spawn($cmd, $env, $opts); - return unless defined $pid; return ($r, $pid) if wantarray; my $ret = gensym; tie *$ret, 'PublicInbox::ProcessPipe', $pid, $r; $ret; } +sub run_die ($;$$) { + my ($cmd, $env, $rdr) = @_; + my $pid = spawn($cmd, $env, $rdr); + waitpid($pid, 0) == $pid or die "@$cmd did not finish"; + $? == 0 or die "@$cmd failed: \$?=$?\n"; +} + 1;