rbb 99/02/03 09:50:15
Modified: pthreads/src/main Makefile.tmpl alloc.c http_main.c pthreads/src/modules/standard mod_autoindex.c pthreads/src/os/unix multithread.c Added: pthreads/src/include fdqueue.h pthreads/src/main fdqueue.c Log: Port to hybrid process/thread model. Doesn't work fully, but it will serve one request before dying. Basically the process that serves the first request becomes a zombie, so the subsequent requests are never picked up off the fd queue. Submitted by: fdqueue.[ch] submitted by Manoj KAsichainula Revision Changes Path 1.1 apache-apr/pthreads/src/include/fdqueue.h Index: fdqueue.h =================================================================== #ifndef FDQUEUE_H #define FDQUEUE_H #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <sys/types.h> #include <sys/socket.h> /* Bleccch. 0 on success always rubbed me the wrong way */ /* All failures are unrecoverable */ #define FD_QUEUE_SUCCESS 0 #define FD_QUEUE_FAILURE -1 /* Needs to be an invalid file descriptor because of queue_pop semantics */ typedef struct fd_queue_elem { int fd; struct sockaddr addr; } FDQueueElement; typedef struct fd_queue { int head; int tail; FDQueueElement *data; int bounds; pthread_mutex_t one_big_mutex; pthread_cond_t not_empty; pthread_cond_t not_full; } FDQueue; int queue_init(FDQueue *queue, size_t bounds); void queue_destroy(FDQueue *queue); int queue_push(FDQueue *queue, int fd, struct sockaddr *addr); int queue_pop(FDQueue *queue, struct sockaddr *addr); int queue_size(FDQueue *queue); #endif /* FDQUEUE_H */ 1.2 +1 -1 apache-apr/pthreads/src/main/Makefile.tmpl Index: Makefile.tmpl =================================================================== RCS file: /home/cvs/apache-apr/pthreads/src/main/Makefile.tmpl,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- Makefile.tmpl 1999/01/21 23:08:32 1.1 +++ Makefile.tmpl 1999/02/03 17:50:08 1.2 @@ -11,7 +11,7 @@ http_config.o http_core.o http_log.o \ http_main.o http_protocol.o http_request.o http_vhost.o \ util.o util_date.o util_script.o util_uri.o util_md5.o \ - md5c.o rfc1413.o + md5c.o rfc1413.o fdqueue.o .c.o: $(CC) -c $(INCLUDES) $(CFLAGS) $< 1.2 +7 -7 apache-apr/pthreads/src/main/alloc.c Index: alloc.c =================================================================== RCS file: /home/cvs/apache-apr/pthreads/src/main/alloc.c,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- alloc.c 1999/01/21 23:08:32 1.1 +++ alloc.c 1999/02/03 17:50:09 1.2 @@ -1975,14 +1975,14 @@ if (pipe_in) { close(in_fds[0]); close(in_fds[1]); - } + } if (pipe_out) { close(out_fds[0]); close(out_fds[1]); - } + } errno = save_errno; return 0; - } + } #ifdef WIN32 @@ -2077,10 +2077,10 @@ close(out_fds[0]); close(out_fds[1]); } - if (pipe_err) { + if (pipe_err) { close(err_fds[0]); close(err_fds[1]); - } + } errno = save_errno; return 0; } @@ -2101,11 +2101,11 @@ close(in_fds[0]); } - if (pipe_err) { + if (pipe_err) { close(err_fds[0]); dup2(err_fds[1], STDERR_FILENO); close(err_fds[1]); - } + } /* HP-UX SIGCHLD fix goes here, if someone will remind me what it is... */ signal(SIGCHLD, SIG_DFL); /* Was that it? */ 1.5 +272 -157 apache-apr/pthreads/src/main/http_main.c Index: http_main.c =================================================================== RCS file: /home/cvs/apache-apr/pthreads/src/main/http_main.c,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- http_main.c 1999/01/28 21:54:07 1.4 +++ http_main.c 1999/02/03 17:50:09 1.5 @@ -92,9 +92,16 @@ #include "util_script.h" /* to force util_script.c linking */ #include "util_uri.h" #include "scoreboard.h" +#include "fdqueue.h" #include <poll.h> #include <netinet/tcp.h> #include <stdio.h> + +#ifdef USE_SHMGET_SCOREBOARD +#include <sys/types.h> +#include <sys/ipc.h> +#include <sys/shm.h> +#endif #include "pthread.h" /*#include initialization if any @@ -149,7 +156,7 @@ #ifdef MULTIPLE_GROUPS gid_t group_id_list[NGROUPS_MAX]; #endif -int ap_threads_per_child; +int ap_threads_per_child = 5; /* Need to require this directive, or make a valid default */ int ap_excess_requests_per_child; char *ap_pid_fname; char *ap_scoreboard_fname; @@ -199,16 +206,14 @@ array_header *ap_server_pre_read_config; array_header *ap_server_post_read_config; array_header *ap_server_config_defines; +pool *pchild; /* Pool for httpd child stuff */ + +/* The queue of sockets we've accepted */ +static FDQueue csd_queue; /* stuff that needs thread local store in main */ typedef struct { - - - jmp_buf thread_exit; - pool *pchild; /* Pool for httpd child stuff */ - int my_child_num; - int requests_this_child; int generation; } tls_main_t; @@ -230,7 +235,6 @@ /* used by accept_loop(), which is serialized */ -static pthread_mutex_t accept_loop_lock; /* Replace AcceptLoopLock with abstract type */ static listen_rec *head_listener; /* *Non*-shared http_main globals... */ @@ -274,10 +278,11 @@ static pool *ptemp; /* Pool for temporart config stuff */ static pool *pcommands; /* Pool for -C and -c switches */ -scoreboard *ap_scoreboard_image = NULL; +static int my_pid; /* for hybridization, we need this. Stupid to + call getpid all the time */ +static int requests_this_child; - - +scoreboard *ap_scoreboard_image = NULL; /* * Pieces for managing the contents of the Server response header @@ -306,6 +311,101 @@ return (server_version ? server_version : SERVER_BASEVERSION); } +#if defined(USE_SHMGET_SCOREBOARD) +static key_t shmkey = IPC_PRIVATE; +static int shmid = -1; + +static void setup_shared_mem(pool *p) +{ + struct shmid_ds shmbuf; +#ifdef MOVEBREAK + char *obrk; +#endif + + if ((shmid = shmget(shmkey, SCOREBOARD_SIZE, IPC_CREAT | SHM_R | SHM_W)) == +-1) { +#ifdef LINUX + if (errno == ENOSYS) { + ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_EMERG, server_conf, + "Your kernel was built without CONFIG_SYSVIPC\n" + "%s: Please consult the Apache FAQ for details", + ap_server_argv0); + } +#endif + ap_log_error(APLOG_MARK, APLOG_EMERG, server_conf, + "could not call shmget"); + exit(APEXIT_INIT); + } + + ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_INFO, server_conf, + "created shared memory segment #%d", shmid); + +#ifdef MOVEBREAK + /* + * Some SysV systems place the shared segment WAY too close + * to the dynamic memory break point (sbrk(0)). This severely + * limits the use of malloc/sbrk in the program since sbrk will + * refuse to move past that point. + * + * To get around this, we move the break point "way up there", + * attach the segment and then move break back down. Ugly + */ + if ((obrk = sbrk(MOVEBREAK)) == (char *) -1) { + ap_log_error(APLOG_MARK, APLOG_ERR, server_conf, + "sbrk() could not move break"); + } +#endif + +#define BADSHMAT ((scoreboard *)(-1)) + if ((ap_scoreboard_image = (scoreboard *) shmat(shmid, 0, 0)) == BADSHMAT) { + ap_log_error(APLOG_MARK, APLOG_EMERG, server_conf, "shmat error"); + /* + * We exit below, after we try to remove the segment + */ + } + else { /* only worry about permissions if we attached t +he segment */ + if (shmctl(shmid, IPC_STAT, &shmbuf) != 0) { + ap_log_error(APLOG_MARK, APLOG_ERR, server_conf, + "shmctl() could not stat segment #%d", shmid); + } + else { + shmbuf.shm_perm.uid = ap_user_id; + shmbuf.shm_perm.gid = ap_group_id; + if (shmctl(shmid, IPC_SET, &shmbuf) != 0) { + ap_log_error(APLOG_MARK, APLOG_ERR, server_conf, + "shmctl() could not set segment #%d", shmid); + } + } + } + /* + * We must avoid leaving segments in the kernel's + * (small) tables. + */ + if (shmctl(shmid, IPC_RMID, NULL) != 0) { + ap_log_error(APLOG_MARK, APLOG_WARNING, server_conf, + "shmctl: IPC_RMID: could not remove shared memory segment #%d", + shmid); + } + if (ap_scoreboard_image == BADSHMAT) /* now bailout */ + exit(APEXIT_INIT); + +#ifdef MOVEBREAK + if (obrk == (char *) -1) + return; /* nothing else to do */ + if (sbrk(-(MOVEBREAK)) == (char *) -1) { + ap_log_error(APLOG_MARK, APLOG_ERR, server_conf, + "sbrk() could not move break back"); + } +#endif + /* ap_scoreboard_image->global.running_generation = 0;*/ +} + +static void reopen_scoreboard(pool *p) +{ +} +#endif + API_EXPORT(void) ap_add_version_component(const char *component) { if (! version_locked) { @@ -351,13 +451,13 @@ } } -/* a clean exit from a child with proper cleanup */ -static void clean_child_exit(int code) __attribute__ ((noreturn)); +/* a clean exit from a child with proper cleanup + static void clean_child_exit(int code) __attribute__ ((noreturn)); */ static void clean_child_exit(int code) { - if (tls()->pchild) { - ap_child_exit_modules(tls()->pchild, server_conf); - ap_destroy_pool(tls()->pchild); + if (pchild) { + ap_child_exit_modules(pchild, server_conf); + ap_destroy_pool(pchild); } /* longjump(tls()->thread_exit, 1); */ } @@ -1148,8 +1248,6 @@ break; lr = lr->next; } - /* turn the list into a ring */ - lr->next = ap_listeners; close_unused_listeners(); listenfds = ap_palloc(p, sizeof(struct pollfd)); @@ -1332,66 +1430,10 @@ /* ZZZ doesn't really make sense in a threaded server. */ } -static int accept_loop(struct sockaddr *sa_client) /* ZZZZ */ -{ - int srv; - listen_rec *lr; - int sd; /* filedesc */ - int csd = 0; /* filedesc */ - ulong len = sizeof(struct sockaddr); - - pthread_mutex_lock(&accept_loop_lock); - for(;;) { - if (num_listenfds > 1) { - /* more than one socket */ - - srv = poll(listenfds, num_listenfds, -1); /* INF_TIMEOUT */ - if (srv < 0) { - pthread_mutex_unlock(&accept_loop_lock); - ap_log_error(APLOG_MARK, APLOG_ERR, server_conf, "select: (listen)"); - clean_child_exit(1); - } - - if (srv == 0) - continue; - - /* find a listner */ - lr = head_listener; - do { - /* XXX: should we check for poll error? */ - if (listenfds[lr->index].revents & POLLIN) { /* ready to read?*/ - /* advance to the next listner for next loop */ - head_listener = lr->next; - goto got_ir; - } - lr = lr->next; - } while (lr != head_listener); - /* if we don;t find anything then just start again */ - continue; - -got_ir: - sd = lr->fd; - } - else { - /* only one socket, just pretent we did the other stuff */ - sd = ap_listeners->fd; - } - - csd = accept(sd, sa_client, &len); - - if (csd >= 0) - break; /* We have a socket ready for reading */ - - /* XXX: deal with error conditions... */ - } - pthread_mutex_unlock(&accept_loop_lock); - return csd; -} - static void process_socket(pool *ptrans, struct sockaddr *sa_client, int csd, int my_child_num) /* ZZZZ */ { - struct sockaddr sa_server; /* ZZZZ */ - ulong len = sizeof(struct sockaddr); + struct sockaddr sa_server; /* ZZZZ */ + size_t len = sizeof(struct sockaddr); BUFF *conn_io; request_rec *r; conn_rec *current_conn; @@ -1469,82 +1511,153 @@ #endif } -static void child_main(int child_num_arg, tls_main_t *t) +void * accept_thread(void * sd) { + int csd = 0; + int * temp = sd; struct sockaddr sa_client; - pool *ptrans; - int csd; + size_t len = sizeof(struct sockaddr); - /* All of initialization is a critical section, we don't care if we're - * told to HUP or USR1 before we're done initializing. For example, - * we could be half way through child_init_modules() when a restart - * signal arrives, and we'd have no real way to recover gracefully - * and exit properly. - * - * I suppose a module could take forever to initialize, but that would - * be either a broken module, or a broken configuration (i.e. network - * problems, file locking problems, whatever). -djg - */ + for (;;) { + csd = accept(*temp, &sa_client, &len); + if (csd >= 0) { + if (queue_push(&csd_queue, csd, &sa_client) != 0) { + /* ap_log_error*/ + } + } else{ + /* ap_log_error()*/ + } + /* thread_exit */ + } +} - csd = -1; - t->my_child_num = child_num_arg; - t->requests_this_child = 0; +void * worker_thread(void * dummy) +{ + int my_pid = *(int *)dummy; + struct sockaddr sa_client; - /* Get a sub pool for global allocations in this child, so that - * we can have cleanups occur when the child exits. - */ - ptrans = t->pchild = ap_make_sub_pool(pconf); + for (;;) { + int csd = queue_pop(&csd_queue, &sa_client); + pthread_t tid = pthread_self(); + process_socket(pchild, &sa_client, csd, my_pid); + } +} + +static void set_group_privs(void) +{ +#ifndef WIN32 + if (!geteuid()) { + char *name; - fprintf(stderr, "%d child_main: status ready\n", child_num_arg); + /* Get username if passed as a uid */ - (void) ap_update_child_status(child_num_arg, SERVER_READY, (request_rec*) NULL); + if (ap_user_name[0] == '#') { + struct passwd *ent; + uid_t uid = atoi(&ap_user_name[1]); + + if ((ent = getpwuid(uid)) == NULL) { + ap_log_error(APLOG_MARK, APLOG_ALERT, server_conf, + "getpwuid: couldn't determine user name from uid %u, " + "you probably need to modify the User directive", + (unsigned)uid); + clean_child_exit(APEXIT_CHILDFATAL); + } - while (1) { + name = ent->pw_name; + } + else + name = ap_user_name; - ap_clear_pool(ptrans); +#ifndef OS2 + /* OS/2 dosen't support groups. */ - (void) ap_update_child_status(child_num_arg, SERVER_READY, (request_rec *) NULL); - csd = accept_loop(&sa_client); - fprintf(stderr, "%d child main: got accept\n", child_num_arg); - process_socket(ptrans, &sa_client, csd, child_num_arg); - + /* Reset `groups' attributes. */ + + if (initgroups(name, ap_group_id) == -1) { + ap_log_error(APLOG_MARK, APLOG_ALERT, server_conf, + "initgroups: unable to set groups for User %s " + "and Group %u", name, (unsigned)ap_group_id); + clean_child_exit(APEXIT_CHILDFATAL); + } +#ifdef MULTIPLE_GROUPS + if (getgroups(NGROUPS_MAX, group_id_list) == -1) { + ap_log_error(APLOG_MARK, APLOG_ALERT, server_conf, + "getgroups: unable to get group list"); + clean_child_exit(APEXIT_CHILDFATAL); + } +#endif + if (setgid(ap_group_id) == -1) { + ap_log_error(APLOG_MARK, APLOG_ALERT, server_conf, + "setgid: unable to set group id to Group %u", + (unsigned)ap_group_id); + clean_child_exit(APEXIT_CHILDFATAL); + } +#endif } +#endif /* ndef WIN32 */ } -static tls_main_t *create_tls(void) +static void child_main(int child_num_arg) { - tls_main_t *t; - int rc; - t = malloc(sizeof(*t)); - memset(t, 0, sizeof(*t)); - if (ap_scoreboard_image) { - t->generation = ap_scoreboard_image->global.exit_generation; - } - - rc = pthread_setspecific(tls_main_key, t); - - if (rc) { - fprintf(stderr, "pthread_setspecific error\n"); - exit(1); + listen_rec *lr; + int i; + pthread_t thread; + int my_child_num = child_num_arg; + + + my_pid = getpid(); + requests_this_child = 0; + + pchild = ap_make_sub_pool(pconf); + + /*stuff to do before we switch id's, so we have permissions.*/ + reopen_scoreboard(pchild); + /* SAFE_ACCEPT(accept_mutex_child_init(pchild));*/ + + set_group_privs(); + + if (!geteuid() && (setuid(ap_user_id) == -1)) { + ap_log_error(APLOG_MARK, APLOG_ALERT, server_conf, + "setuid: unable to change uid"); + clean_child_exit(APEXIT_CHILDFATAL); } - - return t; -} -static void *child_starter(void *dummy) -{ - tls_main_t *t; - - t = create_tls(); - if (!setjmp(t->thread_exit)) { - child_main((int)dummy, t); + ap_child_init_modules(pchild, server_conf); + + /*done with init critical section */ + + queue_init(&csd_queue, 2); + + /* Setup worker threads */ + + for (i=0; i < ap_threads_per_child; i++) { + if (pthread_create(&thread, NULL, worker_thread, &my_child_num)) { + ap_log_error(APLOG_MARK, APLOG_ALERT, server_conf, + "pthread_create: unable to create worker thread"); + exit(1); + } else { + /* UPDATE_SCOREBOARD with thread id and state */ + } + } + /* Setup acceptor threads */ + + lr = ap_listeners; + while (lr != NULL) { + if (pthread_create(&thread, NULL, accept_thread, &(lr->fd))) { + ap_log_error(APLOG_MARK, APLOG_ALERT, server_conf, + "pthread_create: unable to create acceptor thread"); + exit(1); + } else { + /* UPDATE_SCOREBOARD with thread id and state */ + } + lr = lr->next; } } static int make_child(server_rec *s, int slot, time_t now) /* ZZZ */ { pthread_t tid; /* ZZZZ */ - + int pid; int rc; if (slot + 1 > max_daemons_limit) { @@ -1552,19 +1665,15 @@ } if (one_process) { - child_main(slot, tls()); + set_signals(); + child_main(slot); } + Explain1("Starting new child in slot %d",slot); (void) ap_update_child_status(slot, SERVER_STARTING, (request_rec *) NULL); - - /*tid = CreateThread(threadtype, child_starter, (void *)slot, */ - /* priority, ????);*/ - rc = pthread_create(&tid, NULL, child_starter, (void*) slot); - - if (rc) { - printf("pthread_create failed - %d\n", rc); - ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, s, "pthread_create() failed"); + if ((pid = fork()) == -1) { + ap_log_error(APLOG_MARK, APLOG_ERR, s, "fork: Unable to fork new process"); /* fork didn't succeed. Fix the scoreboard or else * it will say SERVER_STARTING forever and ever @@ -1579,11 +1688,30 @@ return -1; } - ap_scoreboard_image->parent[slot].tid = tid; + if (!pid) { +#ifdef AIX_BIND_PROCESSOR + /* By default, AIX binds to a single processor. This bit unbinds + children which will then bind to another CPU. + */ +#include <sys/processor.h> + int status = bindprocessor(BINDPROCESS, (int)getpid(), + PROCESSOR_CLASS_ANY); + if (status != OK) + ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_WARNING, server_conf, + "processor unbind failed %d", status); +#endif + + RAISE_SIGSTOP(MAKE_CHILD); + MONCONTROL(1); + set_signals(); + + child_main(slot); - return 0; -} + ap_scoreboard_image->parent[slot].pid = pid; + return 0; + } +} /* start up a bunch of children */ static void startup_children(int number_to_start) @@ -1742,7 +1870,7 @@ pid = -1; if (pid >= 0) { - child_slot = find_child_by_tid(NULL); + child_slot = find_child_by_tid(0); if (child_slot >= 0) { (void) ap_update_child_status(child_slot, SERVER_DEAD, (request_rec *) NULL); @@ -1818,11 +1946,9 @@ reinit_scoreboard(pconf); } - set_signals(); - /* set up accept_loop */ + /* set up get_socket */ head_listener = ap_listeners; - pthread_mutex_init(&accept_loop_lock, NULL); if (ap_daemons_max_free < ap_daemons_min_free + 1) /* Don't thrash... */ ap_daemons_max_free = ap_daemons_min_free + 1; @@ -1906,8 +2032,6 @@ "SIGHUP received. Attempting to restart"); } - pthread_mutex_destroy(&accept_loop_lock); - } static void standalone_main(int argc, char **argv) @@ -1941,15 +2065,6 @@ int c; int rc; /* ZZZZ */ int configtestonly; - - /* Create thread local storage key */ - rc = pthread_key_create(&tls_main_key, NULL); - if (rc) { - perror("pthread_key_create failed\n"); - exit(1); - } - - create_tls(); MONCONTROL(0); /* WTF is this? */ 1.1 apache-apr/pthreads/src/main/fdqueue.c Index: fdqueue.c =================================================================== #include "fdqueue.h" /* XXX - We should get rid of the malloc and free used here. This should be * ap_palloc, I guess (mvsk) */ /* Assumption: queue itself is allocated by the user */ /* Assumption: increment and decrement are atomic on int */ int queue_init(FDQueue *queue, size_t bounds) { pthread_mutex_init(&queue->one_big_mutex, NULL); pthread_cond_init(&queue->not_empty, NULL); pthread_cond_init(&queue->not_full, NULL); queue->head = queue->tail = 0; queue->data = malloc(bounds * sizeof(FDQueueElement)); queue->bounds = bounds; return FD_QUEUE_SUCCESS; } void queue_destroy(FDQueue *queue) { free(queue->data); /* Ignore errors here, we can't do anything about them anyway */ pthread_cond_destroy(&queue->not_empty); pthread_cond_destroy(&queue->not_full); pthread_mutex_destroy(&queue->one_big_mutex); } int queue_push(FDQueue *queue, int fd, struct sockaddr *addr) { if (pthread_mutex_lock(&queue->one_big_mutex) != 0) { return FD_QUEUE_FAILURE; } while (queue->head == ((queue->tail + 1) % queue->bounds)) { pthread_cond_wait(&queue->not_full, &queue->one_big_mutex); } queue->data[queue->tail].fd = fd; queue->data[queue->tail].addr = *addr; queue->tail = (queue->tail + 1) % queue->bounds; if (pthread_mutex_unlock(&queue->one_big_mutex) != 0) { return FD_QUEUE_FAILURE; } if (pthread_cond_signal(&queue->not_empty) != 0) perror("signal didn't work :("); return FD_QUEUE_SUCCESS; } int queue_pop(FDQueue *queue, struct sockaddr *addr) { int fd; if (pthread_mutex_lock(&queue->one_big_mutex) != 0) { return FD_QUEUE_FAILURE; } while (queue->head == queue->tail) { pthread_cond_wait(&queue->not_empty, &queue->one_big_mutex); } fd = queue->data[queue->head].fd; *addr = queue->data[queue->head].addr; queue->head = (queue->head + 1) % queue->bounds; if (pthread_mutex_unlock(&queue->one_big_mutex) != 0) { return FD_QUEUE_FAILURE; } pthread_cond_signal(&queue->not_full); return fd; } int queue_size(FDQueue *queue) { return ((queue->tail - queue->head + queue->bounds) % queue->bounds); } 1.3 +2 -1 apache-apr/pthreads/src/modules/standard/mod_autoindex.c Index: mod_autoindex.c =================================================================== RCS file: /home/cvs/apache-apr/pthreads/src/modules/standard/mod_autoindex.c,v retrieving revision 1.2 retrieving revision 1.3 diff -u -r1.2 -r1.3 --- mod_autoindex.c 1999/01/22 17:10:28 1.2 +++ mod_autoindex.c 1999/02/03 17:50:12 1.3 @@ -884,7 +884,8 @@ p->ascending = (ap_toupper(direction) == D_ASCENDING); if (autoindex_opts & FANCY_INDEXING) { - request_rec *rr = ap_sub_req_lookup_file(name, r); + request_rec *rr = ap_sub_req_lookup_file(name, r); + fprintf(stderr, "sub_req:::: %s\n", name); if (rr->finfo.st_mode != 0) { p->lm = rr->finfo.st_mtime; 1.3 +2 -2 apache-apr/pthreads/src/os/unix/multithread.c Index: multithread.c =================================================================== RCS file: /home/cvs/apache-apr/pthreads/src/os/unix/multithread.c,v retrieving revision 1.2 retrieving revision 1.3 diff -u -r1.2 -r1.3 --- multithread.c 1999/01/27 16:15:49 1.2 +++ multithread.c 1999/02/03 17:50:14 1.3 @@ -37,8 +37,8 @@ if ((pid = fork()) == -1) { return pid; } else if (pid == 0) { - execvp(file, argv); - return -1; + if (execvp(file, argv) == -1) + return -1; } else return pid; }