From: Daniel J Walsh <dwalsh@redhat.com>
To: russell@coker.com.au
Cc: SE-Linux <selinux@tycho.nsa.gov>
Subject: Re: SMP enabled restorecon
Date: Wed, 02 Aug 2006 15:52:01 -0400 [thread overview]
Message-ID: <44D10261.4020005@redhat.com> (raw)
In-Reply-To: <200607310012.02943.russell@coker.com.au>
Russell Coker wrote:
> On Sunday 30 July 2006 23:13, Russell Coker <russell@coker.com.au> wrote:
>
>> On Sunday 30 July 2006 01:17, Russell Coker <russell@coker.com.au> wrote:
>>
>>> Below are the results of running the FC5+updates version of restorecon vs
>>> my patched version with SMP support on a PentiumD-2.8GHz model 920. The
>>> elapsed time is cut by 40%! The user time reported appears to be a bug
>>> in the time command, when I observe the run via the "top" command I see
>>> two restorecon processes each taking about 26 seconds of CPU time before
>>> they complete.
>>>
>> I've attached a similar patch for setfiles, this patch adds a "-a" option
>> to setfiles to skip the association check because it's really difficult to
>> preserve that check while supporting SMP and giving good performance, and
>> also because checking for associations isn't as useful when the results go
>> to /dev/null anyway.
>>
>
> Sorry for replying to my own mail so much, I've been involved in discussion on
> IRC and other lists.
>
> I've attached a new patch which calls wait() for each child process. This
> causes CPU time use to be correctly billed to parent processes, the results
> are as follows:
>
> Modified setfiles:
> real 1m22.689s
> user 1m13.181s
> sys 0m12.641s
>
> Orig setfiles:
> real 1m23.697s
> user 0m59.916s
> sys 0m4.892s
>
> Modified restorecon on /usr/share:
> real 0m31.142s
> user 0m52.671s
> sys 0m7.728s
>
> Orig restorecon:
> real 0m51.429s
> user 0m45.467s
> sys 0m5.568s
>
> It seems that in both cases the amount of CPU time used is significantly
> increased, and that the variance between test runs of setfiles is great
> enough that there can be little performance difference between my version and
> the stock one.
>
> I think that I need to find a more CPU efficient method of IPC for the file
> names. One possibility might be to use shared memory regions for the data.
> Another one might be to make the programs threaded.
>
> A final possibility is that the current design of setfiles and restorecon may
> be a dead-end as far as SMP is concerned. Having one process to stat the
> files and another to check the contexts when the cache is hot is efficient
> for UP systems. For SMP systems it might be better to have one process find
> the files and then pass the names to worker processes that do the regex
> stuff. Currently processing a file involves two processes calling nftw()
> (doing readdir() and stat()), the seeker writes a byte to a pipe and then the
> master will either pipe the file name to a child or process it directly
> before reading the byte from the seek process. As I have the worker
> acknowledging each file the average number of pipe writes to process a file
> on a 2P system is two.
>
> If the parent process was to be the only process calling nftw() and it then
> passed the file names via shared memory with specified minimum and maximum
> sizes for the shared memory we could probably get an average of considerably
> less than one IPC operation per file that is processes.
>
>
> ------------------------------------------------------------------------
>
> diff -ru policycoreutils-1.30.10.orig/restorecon/restorecon.c policycoreutils-1.30.10/restorecon/restorecon.c
> --- policycoreutils-1.30.10.orig/restorecon/restorecon.c 2006-05-23 20:20:02.000000000 +1000
> +++ policycoreutils-1.30.10/restorecon/restorecon.c 2006-07-30 23:26:21.000000000 +1000
> @@ -34,6 +34,7 @@
> #include <selinux/selinux.h>
> #include <getopt.h>
> #include <sys/types.h>
> +#include <sys/wait.h>
> #include <sys/stat.h>
> #include <fcntl.h>
> #include <stdio.h>
> @@ -50,6 +51,15 @@
> #define STAT_BLOCK_SIZE 1
> static int pipe_fds[2] = { -1, -1 };
>
> +#define WORKER_SUCCESS 'g'
> +#define WORKER_FAILURE 'b'
> +int *worker_send_fds = NULL;
> +int *worker_recv_fds = NULL;
> +int num_workers = 0;
> +int max_worker_handle = 0;
> +int *worker_use_count = NULL;
> +#define MAX(XA, XB) ((XA) > (XB)) ? (XA) : (XB)
> +
> #define MAX_EXCLUDES 100
> static int excludeCtr=0;
> struct edir {
> @@ -241,32 +251,244 @@
> return 0;
> }
>
> +void get_worker_data(int finishing)
> +{
> + int i, count;
> + fd_set read_set;
> + struct timeval tv;
> + tv.tv_sec = finishing ? 1 : 0;
> + tv.tv_usec = 0;
> +
> + FD_ZERO(&read_set);
> + for(i = 0; i < num_workers; i++)
> + FD_SET(worker_recv_fds[i], &read_set);
> +
> + count = select(max_worker_handle, &read_set, NULL, NULL, &tv);
> + if(count == 0)
> + return;
> + if(count == -1)
> + {
> + perror("Worker select error");
> + exit(1);
> + }
> + for(i = 0; i < num_workers; i++)
> + {
> + if(FD_ISSET(worker_recv_fds[i], &read_set))
> + {
> + char c;
> + int rc = read(worker_recv_fds[i], &c, 1);
> + if(rc != 1)
> + {
> + fprintf(stderr, "Error reading result from worker %d, aborting.\n", i);
> + exit(1);
> + }
> + if(c != WORKER_SUCCESS)
> + errors++;
> + worker_use_count[i]--;
> + count--;
> + }
> + }
> + if(count != 0)
> + {
> + fprintf(stderr, "Count should be 0!\n");
> + exit(1);
> + }
> +}
> +
> static int apply_spec(const char *file,
> const struct stat *sb_unused __attribute__((unused)),
> int flag,
> struct FTW *s_unused __attribute__((unused)))
> {
> - char buf[STAT_BLOCK_SIZE];
> - if(pipe_fds[0] != -1 && read(pipe_fds[0], buf, STAT_BLOCK_SIZE) != STAT_BLOCK_SIZE)
> - {
> - fprintf(stderr, "Read error on pipe.\n");
> - pipe_fds[0] = -1;
> - }
> - if (flag == FTW_DNR) {
> - fprintf(stderr, "%s: unable to read directory %s\n",
> - progname, file);
> - return 0;
> - }
> - errors=errors+restore(file);
> - return 0;
> + char buf[STAT_BLOCK_SIZE];
> +
> + if(pipe_fds[0] != -1 && read(pipe_fds[0], buf, STAT_BLOCK_SIZE) != STAT_BLOCK_SIZE)
> + {
> + fprintf(stderr, "Read error on pipe.\n");
> + pipe_fds[0] = -1;
> + }
> + if (flag == FTW_DNR) {
> + fprintf(stderr, "%s: unable to read directory %s\n",
> + progname, file);
> + return 0;
> + }
> + if(num_workers)
> + {
> + int i, rc;
> + get_worker_data(0);
> + for(i = 0; i < num_workers; i++)
> + {
> + /* Have two entries in the queue for each worker so that if the master
> + starts doing some regex work the workers don't starve */
> + if(worker_use_count[i] < 2)
> + {
> + int offset = 0, len = strlen(file) + 1;
> +
> + worker_use_count[i]++;
> + /* Could hurt performance if FILE_MAX*2 > the max pipe buffer */
> + while(1)
> + {
> + rc = write(worker_send_fds[i], &file[offset], len - offset);
> + if(rc == -1)
> + {
> + fprintf(stderr, "Can't write to worker %d\n", i);
> + exit(1);
> + }
> + offset += rc;
> + if(offset == len)
> + return 0;
> + }
> + }
> + }
> + }
> + errors=errors+restore(file);
> + return 0;
> }
> +
> +int num_cpus()
> +{
> + FILE *fp = fopen("/proc/cpuinfo", "r");
> + char buf[1024];
> + int num = 0;
> +
> + if(!fp)
> + return 1;
> + while(fgets(buf, sizeof(buf), fp))
> + {
> + if(!strncmp(buf, "processor", 9))
> + num++;
> + }
> + fclose(fp);
> + if(num < 1)
> + return 1;
> + return num;
> +}
> +
> +void worker_restore(int id, const char * const file, int fd_out)
> +{
> + char res = WORKER_SUCCESS;
> + if(restore(file))
> + {
> + res = WORKER_FAILURE;
> + }
> + if(write(fd_out, &res, 1) != 1)
> + {
> + fprintf(stderr, "Worker %d can't write to result pipe, aborting.\n", id);
> + exit(1);
> + }
> +}
> +
> +void worker(int id, int fd_in, int fd_out)
> +{
> + char buf[PATH_MAX+1];
> + int len = 0;
> +
> + while(1)
> + {
> + while(len == 0 || buf[len - 1] != '\0')
> + {
> + int string_len;
> + int rc = read(fd_in, buf + len, PATH_MAX - len);
> + if(rc == -1)
> + {
> + fprintf(stderr, "Worker %d exiting on read error.\n", id);
> + exit(1);
> + }
> + if(rc == 0)
> + exit(0); /* parent has exited */
> + len += rc;
> + buf[len] = '\0';
> + string_len = strlen(buf);
> + while(string_len != len)
> + {
> + worker_restore(id, buf, fd_out);
> + len = len - string_len - 1;
> + memmove(buf, buf + string_len + 1, len + 1);
> + string_len = strlen(buf);
> + }
> + if(len >= PATH_MAX)
> + {
> + fprintf(stderr, "Worker %d received excess data from parent.\n", id);
> + exit(1);
> + }
> + }
> + worker_restore(id, buf, fd_out);
> + len = 0;
> + }
> + if(len != 0)
> + {
> + fprintf(stderr, "Worker %d can't read from pipe, aborting.\n", id);
> + exit(1);
> + }
> + exit(0);
> +}
> +
> +void start_workers()
> +{
> + int send_fds[2], recv_fds[2], i;
> +
> + num_workers = num_cpus() - 1;
> + if(!num_workers)
> + return;
> + worker_send_fds = malloc(sizeof(int) * num_workers);
> + worker_recv_fds = malloc(sizeof(int) * num_workers);
> + worker_use_count = malloc(sizeof(int) * num_workers);
> + if(!worker_send_fds || !worker_recv_fds || !worker_use_count)
> + {
> + fprintf(stderr, "Malloc failure.\n");
> + exit(1);
> + }
> +
> + matchpathcon_init(NULL);
> +
> + for(i = 0; i < num_workers; i++)
> + {
> + int rc;
> +
> + if(pipe(send_fds) || pipe(recv_fds))
> + {
> + fprintf(stderr, "Can't create pipe.\n");
> + exit(1);
> + }
> + worker_send_fds[i] = send_fds[1];
> + worker_recv_fds[i] = recv_fds[0];
> + worker_use_count[i] = 0;
> + max_worker_handle = MAX(max_worker_handle, MAX(worker_send_fds[i], worker_recv_fds[i]));
> + rc = fork();
> + if(rc == -1)
> + {
> + fprintf(stderr, "fork error\n");
> + exit(1);
> + }
> + if(!rc)
> + {
> + int j;
> + for(j = i - 1; j >= 0; j--)
> + {
> + close(worker_send_fds[j]);
> + close(worker_recv_fds[j]);
> + }
> + close(send_fds[1]);
> + close(recv_fds[0]);
> + worker(i, send_fds[0], recv_fds[1]);
> + exit(2); /* should never reach this code */
> + }
> + close(send_fds[0]);
> + close(recv_fds[1]);
> + }
> + max_worker_handle++;
> +}
> +
> void process(char *buf) {
> int rc;
> +
> if (recurse) {
> if(pipe(pipe_fds) == -1)
> - rc = -1;
> - else
> - rc = fork();
> + {
> + fprintf(stderr, "Can't create pipe.\n");
> + exit(1);
> + }
> + rc = fork();
> if(rc == 0)
> {
> close(pipe_fds[0]);
> @@ -276,10 +498,25 @@
> if(rc > 0)
> close(pipe_fds[1]);
> if(rc == -1 || rc > 0) {
> + start_workers();
> if (nftw(buf, apply_spec, 1024, FTW_PHYS)) {
> fprintf(stderr, "%s: error while labeling files under %s\n",
> progname, buf);
> errors++;
> + }
> + if(rc > 0)
> + wait(&rc);
> + if(num_workers)
> + {
> + int i;
> + for(i = 0; i < num_workers; i++)
> + {
> + while(worker_use_count[i])
> + get_worker_data(1);
> + close(worker_send_fds[i]);
> + close(worker_recv_fds[i]);
> + wait(&rc);
> + }
> }
> }
> }
> diff -ru policycoreutils-1.30.10.orig/setfiles/setfiles.c policycoreutils-1.30.10/setfiles/setfiles.c
> --- policycoreutils-1.30.10.orig/setfiles/setfiles.c 2006-05-23 20:20:03.000000000 +1000
> +++ policycoreutils-1.30.10/setfiles/setfiles.c 2006-07-30 23:29:07.000000000 +1000
> @@ -75,6 +75,7 @@
> #include <selinux/selinux.h>
> #include <syslog.h>
> #include <libgen.h>
> +#include <sys/wait.h>
>
> static int add_assoc = 1;
> static FILE *outfile=NULL;
> @@ -82,6 +83,15 @@
> #define STAT_BLOCK_SIZE 1
> static int pipe_fds[2] = { -1, -1 };
>
> +#define WORKER_SUCCESS 'g'
> +#define WORKER_FAILURE 'b'
> +int *worker_send_fds = NULL;
> +int *worker_recv_fds = NULL;
> +int num_workers = 0;
> +int max_worker_handle = 0;
> +int *worker_use_count = NULL;
> +#define MAX(XA, XB) ((XA) > (XB)) ? (XA) : (XB)
> +
> #define MAX_EXCLUDES 100
> static int excludeCtr=0;
> struct edir {
> @@ -232,27 +242,57 @@
> return (strcmp(rest_a, rest_b) == 0);
> }
>
> -/*
> - * Apply the last matching specification to a file.
> - * This function is called by nftw on each file during
> - * the directory traversal.
> - */
> -static int apply_spec(const char *file,
> - const struct stat *sb_unused __attribute__((unused)),
> - int flag,
> - struct FTW *s_unused __attribute__((unused)))
> +void get_worker_data(int finishing)
> +{
> + int i, count;
> + fd_set read_set;
> + struct timeval tv;
> + tv.tv_sec = finishing ? 1 : 0;
> + tv.tv_usec = 0;
> +
> + FD_ZERO(&read_set);
> + for(i = 0; i < num_workers; i++)
> + FD_SET(worker_recv_fds[i], &read_set);
> +
> + count = select(max_worker_handle, &read_set, NULL, NULL, &tv);
> + if(count == 0)
> + return;
> + if(count == -1)
> + {
> + perror("Worker select error");
> + exit(1);
> + }
> + for(i = 0; i < num_workers; i++)
> + {
> + if(FD_ISSET(worker_recv_fds[i], &read_set))
> + {
> + char c;
> + int rc = read(worker_recv_fds[i], &c, 1);
> + if(rc != 1)
> + {
> + fprintf(stderr, "Error reading result from worker %d, aborting.\n", i);
> + exit(1);
> + }
> +/* if(c != WORKER_SUCCESS)
> + errors++;*/
> + worker_use_count[i]--;
> + count--;
> + }
> + }
> + if(count != 0)
> + {
> + fprintf(stderr, "Count should be 0!\n");
> + exit(1);
> + }
> +}
> +
> +static int set_context(const char *file)
> {
> const char *my_file;
> struct stat my_sb;
> int i, j, ret;
> char *context, *newcon;
> int user_only_changed=0;
> - char buf[STAT_BLOCK_SIZE];
> - if(pipe_fds[0] != -1 && read(pipe_fds[0], buf, STAT_BLOCK_SIZE) != STAT_BLOCK_SIZE)
> - {
> - fprintf(stderr, "Read error on pipe.\n");
> - pipe_fds[0] = -1;
> - }
>
> /* Skip the extra slash at the beginning, if present. */
> if (file[0] == '/' && file[1] == '/')
> @@ -260,12 +300,6 @@
> else
> my_file = file;
>
> - if (flag == FTW_DNR) {
> - fprintf(stderr, "%s: unable to read directory %s\n",
> - progname, my_file);
> - return 0;
> - }
> -
> i = match(my_file, &my_sb, &newcon);
> if (i < 0)
> /* No matching specification. */
> @@ -382,6 +416,198 @@
> return -1;
> }
>
> +/*
> + * Apply the last matching specification to a file.
> + * This function is called by nftw on each file during
> + * the directory traversal.
> + */
> +static int apply_spec(const char *file,
> + const struct stat *sb_unused __attribute__((unused)),
> + int flag,
> + struct FTW *s_unused __attribute__((unused)))
> +{
> + char buf[STAT_BLOCK_SIZE];
> + if(pipe_fds[0] != -1 && read(pipe_fds[0], buf, STAT_BLOCK_SIZE) != STAT_BLOCK_SIZE)
> + {
> + fprintf(stderr, "Read error on pipe.\n");
> + pipe_fds[0] = -1;
> + }
> +
> + if (flag == FTW_DNR)
> + {
> + fprintf(stderr, "%s: unable to read directory %s\n",
> + progname, file);
> + return 0;
> + }
> +
> + if(num_workers)
> + {
> + int i, rc;
> + get_worker_data(0);
> + for(i = 0; i < num_workers; i++)
> + {
> + /* Have two entries in the queue for each worker so that if the master
> + starts doing some regex work the workers don't starve */
> + if(worker_use_count[i] < 2)
> + {
> + int offset = 0, len = strlen(file) + 1;
> +
> + worker_use_count[i]++;
> + /* Could hurt performance if FILE_MAX*2 > the max pipe buffer */
> + while(1)
> + {
> + rc = write(worker_send_fds[i], &file[offset], len - offset);
> + if(rc == -1)
> + {
> + fprintf(stderr, "Can't write to worker %d\n", i);
> + exit(1);
> + }
> + offset += rc;
> + if(offset == len)
> + return 0;
> + }
> + }
> + }
> + }
> + return set_context(file);
> +}
> +
> +int num_cpus()
> +{
> + FILE *fp = fopen("/proc/cpuinfo", "r");
> + char buf[1024];
> + int num = 0;
> +
> + if(!fp)
> + return 1;
> + while(fgets(buf, sizeof(buf), fp))
> + {
> + if(!strncmp(buf, "processor", 9))
> + num++;
> + }
> + fclose(fp);
> + if(num < 1)
> + return 1;
> + return num;
> +}
> +
> +void worker_restore(int id, const char * const file, int fd_out)
> +{
> + char res = WORKER_SUCCESS;
> + if(set_context(file))
> + {
> + res = WORKER_FAILURE;
> + }
> + if(write(fd_out, &res, 1) != 1)
> + {
> + fprintf(stderr, "Worker %d can't write to result pipe, aborting.\n", id);
> + exit(1);
> + }
> +}
> +
> +void worker(int id, int fd_in, int fd_out)
> +{
> + char buf[PATH_MAX+1];
> + int len = 0;
> +
> + while(1)
> + {
> + while(len == 0 || buf[len - 1] != '\0')
> + {
> + int string_len;
> + int rc = read(fd_in, buf + len, PATH_MAX - len);
> + if(rc == -1)
> + {
> + fprintf(stderr, "Worker %d exiting on read error.\n", id);
> + exit(1);
> + }
> + if(rc == 0)
> + exit(0); /* parent has exited */
> + len += rc;
> + buf[len] = '\0';
> + string_len = strlen(buf);
> + while(string_len != len)
> + {
> + worker_restore(id, buf, fd_out);
> + len = len - string_len - 1;
> + memmove(buf, buf + string_len + 1, len + 1);
> + string_len = strlen(buf);
> + }
> + if(len >= PATH_MAX)
> + {
> + fprintf(stderr, "Worker %d received excess data from parent.\n", id);
> + exit(1);
> + }
> + }
> + worker_restore(id, buf, fd_out);
> + len = 0;
> + }
> + if(len != 0)
> + {
> + fprintf(stderr, "Worker %d can't read from pipe, aborting.\n", id);
> + exit(1);
> + }
> + exit(0);
> +}
> +
> +void start_workers()
> +{
> + int send_fds[2], recv_fds[2], i;
> +
> + if(add_assoc)
> + return;
> + num_workers = num_cpus() - 1;
> + if(!num_workers)
> + return;
> + worker_send_fds = malloc(sizeof(int) * num_workers);
> + worker_recv_fds = malloc(sizeof(int) * num_workers);
> + worker_use_count = malloc(sizeof(int) * num_workers);
> + if(!worker_send_fds || !worker_recv_fds || !worker_use_count)
> + {
> + fprintf(stderr, "Malloc failure.\n");
> + exit(1);
> + }
> +
> + matchpathcon_init(NULL);
> +
> + for(i = 0; i < num_workers; i++)
> + {
> + int rc;
> +
> + if(pipe(send_fds) || pipe(recv_fds))
> + {
> + fprintf(stderr, "Can't create pipe.\n");
> + exit(1);
> + }
> + worker_send_fds[i] = send_fds[1];
> + worker_recv_fds[i] = recv_fds[0];
> + worker_use_count[i] = 0;
> + max_worker_handle = MAX(max_worker_handle, MAX(worker_send_fds[i], worker_recv_fds[i]));
> + rc = fork();
> + if(rc == -1)
> + {
> + fprintf(stderr, "fork error\n");
> + exit(1);
> + }
> + if(!rc)
> + {
> + int j;
> + for(j = i - 1; j >= 0; j--)
> + {
> + close(worker_send_fds[j]);
> + close(worker_recv_fds[j]);
> + }
> + close(send_fds[1]);
> + close(recv_fds[0]);
> + worker(i, send_fds[0], recv_fds[1]);
> + exit(2); /* should never reach this code */
> + }
> + close(send_fds[0]);
> + close(recv_fds[1]);
> + }
> + max_worker_handle++;
> +}
> +
> void set_rootpath(const char *arg)
> {
> int len;
> @@ -454,7 +680,7 @@
> set_matchpathcon_flags(MATCHPATHCON_VALIDATE);
>
> /* Process any options. */
> - while ((opt = getopt(argc, argv, "Fc:dlnqrsvWe:o:")) > 0) {
> + while ((opt = getopt(argc, argv, "aFc:dlnqrsvWe:o:")) > 0) {
> switch (opt) {
> case 'c':
> {
> @@ -529,6 +755,7 @@
> break;
> case 's':
> use_stdin = 1;
> + case 'a':
> add_assoc = 0;
> break;
> case 'v':
> @@ -641,6 +868,7 @@
> close(pipe_fds[1]);
> if(rc == -1 || rc > 0) {
>
> + start_workers();
> /* Walk the file tree, calling apply_spec on each file. */
> if (nftw
> (argv[optind], apply_spec, 1024,
> @@ -650,6 +878,20 @@
> argv[0], argv[optind]);
> exit(1);
> }
> + if(rc > 0)
> + wait(&rc);
> + if(num_workers)
> + {
> + int i;
> + for(i = 0; i < num_workers; i++)
> + {
> + while(worker_use_count[i])
> + get_worker_data(1);
> + close(worker_send_fds[i]);
> + close(worker_recv_fds[i]);
> + wait(&rc);
> + }
> + }
> }
>
> /*
>
You might want to look at what find is doing also, since it seems more
efficient that restorecon
time restorecon -R /etc
real 0m5.729s
user 0m1.532s
sys 0m3.576s
time find /etc/ | restorecon -f -
real 0m5.153s
user 0m1.560s
sys 0m3.564s
--
This message was distributed to subscribers of the selinux mailing list.
If you no longer wish to subscribe, send mail to majordomo@tycho.nsa.gov with
the words "unsubscribe selinux" without quotes as the message.
next prev parent reply other threads:[~2006-08-02 19:52 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2006-07-29 15:17 SMP enabled restorecon Russell Coker
2006-07-29 15:20 ` Russell Coker
2006-07-30 13:13 ` Russell Coker
2006-07-30 14:12 ` Russell Coker
2006-08-02 19:52 ` Daniel J Walsh [this message]
2006-08-02 20:25 ` Klaus Weidner
2006-08-02 20:38 ` Stephen Smalley
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=44D10261.4020005@redhat.com \
--to=dwalsh@redhat.com \
--cc=russell@coker.com.au \
--cc=selinux@tycho.nsa.gov \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.