The Joy of execv
The execv system call replaces the current process with a new program. It loads the new program into the current process's address space, and starts executing it from the beginning. It's arguably the most important system call in Unix-like systems, because it's how you start new programs.
execv is tricky for a few reasons, including
- If switching over to the new program fails,
execvreturns an error code to the original program, which can then decide what to do. Thus you can't be too eager to clean up the old program's resources, because you might need to return to it. execvprovides arguments to the new program, but getting those arguments copied over correctly can be a bit of a challenge.
An Example
This example will help show the execv system call in action (it actually uses execve, which includes a pointer for an environment, which we'll ignore for now). Here's a simple program that uses execv to run the ls command:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void
print_args(const char* where, int argc, char *argv[])
{
if (argc >= 0) {
printf("Number of arguments: %d\n", argc);
}
printf("%s these arguments and their memory locations:\n", where);
printf("via argv @ %p\n", (void *) argv);
for (int i = 0; argv[i] != NULL; i++) {
printf(" argv[%d] = %p: %s\n", i, (void *) argv[i], argv[i]);
}
printf("\n");
}
static char prog_name[] = "exec-demo";
static char from_str[] = "from";
static char exec_str[] = "exec";
static char hello_str[] = "Hello";
static char world_str[] = "World";
static char *new_argv[] = {prog_name, hello_str, world_str,
from_str, exec_str, NULL};
/*
* This is actually two programs in one. An "executor" that calls execve to
* run the "executee" program. It can tell which one it is by the number of
* arguments it receives (no arguments means it's the executor).
*/
int
main(int argc, char *argv[])
{
int dummy = 0;
if (argc == 1) {
printf("=== Executor === (stack at %p)\n\n", (void *) &dummy);
/* Mix things up a bit */
new_argv[0] = argv[0];
new_argv[2] = strdup("Universe");
print_args("Sending", -1, new_argv);
printf("Calling execve...\n\n");
execve(argv[0], new_argv, NULL);
// If execve returns, it must have failed
perror("execve");
exit(1);
} else {
printf("=== Executee === (stack at %p)\n\n", (void *) &dummy);
print_args("Received", argc, argv);
}
return 0;
}
I noticed that while in the executor, the arguments were all scattered around in memory, but in the executee, they were all packed together. We had the actual
argvarray first, and then all of its elements in a nice neat row.
I also noticed that they're actually up higher in memory than the stack pointer.
So, the kernel needs to act as a data copier, copying the arguments from the executor's address space to the executee's address space, and packing them all together nicely in one block of memory. We have to find somewhere to put the arguments, and the common convention is to use the stack, pushing the top-of=stack pointer down so that it's underneath all the arguments.
Technically, nothing requires us to put the arguments on the stack. We could put them anywhere in the executee's address space that's safe, as long as we tell the executee where they are.
Why Copying Is a Challenge
Copying data from one user address space to another is a challenge because we can only have one user address space active at a time, which means we can't access both address spaces at the same time, so we can't just do a direct copy. Instead, we have to first copy the data into the kernel, and then copy it back out to the new address space. This limitation is annoying for two reasons:
- We don't know ahead of time how much data we'll need to copy.
- Kernel memory is fairly precious.
- In the case of OS/161,
kmallocis fine for small amounts of memory, but is generally not recommended for large amounts of memory. After OS/161 has been running for a bit, the kernel's heap may be fragmented, andkmallocmay not be able to find a large enough contiguous block of memory.
- In the case of OS/161,
As a result, pretty much all POSIX systems impose a limit, ARG_MAX, on the total size of all the arguments that can be passed to a new program. You can find out what that limit is on a Linux machine or Mac by typing
getconf ARG_MAX
On Linux, it tends to be 2097152 bytes (2 MB), and on a Mac, 1048576 bytes (1 MB).
Doing the Copy in OS/161
For OS/161 we set ARG_MAX to 65536 bytes (64 kB), which is fine for most programs. But even there, 64 kB is a lot of memory to use with kmalloc, so we recommend that you compile a 64 kB buffer into your kernel that you can use for copying arguments, and use appropriate locking to ensure that only one program can use it at a time.
Meh. I'm just so looking forward to implementing this. Not.
Well, it is a pain to do the copy, but you can prototype some of your code outside of OS/161, and then move it in when you're ready.
Meh. Sounds like more work. I'll just make my partner debug it.
(When logged in, completion status appears here.)