Concurrency Using Processes

Created

Concurrency with Processes

Concurrency

Servers can receieve requests from many clients simultaneously.

Concurrency is proccessing involving multiple tasks that execute asynchronously with respect to each other.

  • E.g., multiple server/client conversations could be ongoing at the same time.

Concurrency is distinct from parallelism. Consider two tasks, A and B, consisting of a sequence of instructions.

  • A and B execute concurrently if relative ordering of instructions in A and B is not guaranteed (i.e., an instruction in A could happen either "before" or "after" an instruction iin B).
  • A and B execute in parallel if instructions in A and B can execute at the same time, and this requires multiple processors or cores.
  • Parallelism implies concurrency, but concurrency does not imply parallelism.

Processes

The fork system call makes a new child process that is a duplicate of the parent process.

  • This includes inheriting open files.

Idea: each time the server accepts a connection, fork, a child process to handle communication with that client.

Multiple child processes can be executing concurrently.

  • OS kernel is responsible for allocating CPU time and handling I/O.

Issue: we may want to limit the number of simultaneous child processes.

  • Processes are somewhat heavyweight in terms of system resources.
  • Solution: before starting a child process, the server loop will wait to make sure fewer than the maximum number of child processes are running.

Signal Handler

Several system calls exist to allow a parent process to receieve a child process's exit status (wait, waitpid)

If a child terminates but the parent doesn't wait for it, it can become a zombie

A parent process can handle teh SIGCHILD signal in order to be notified when a child process exits.

Idea: parent keeps count of how many child processes are running, uses wait system call, and SIGCHLD signal handler to detect when child processes complete.

Signal handlers: the signal and sigaction system calls can be used to register a signal handler function for a particular signal.

Signal handler for SIGCHLD signal, so server is notified when a child process terminates.

/* current number of child processes running */
int g_num_procs;

void sigchld_handler(int signo) {
	int wstatus;
	wait(&wstatus);
	if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) {
		g_num_procs--;
	}
}

Registering a signal handler. register the sigchld_handler function as a handler for the SIGCHLD signal:

 struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = sigchld_handler;
sigaction(SIGCHLD, &sa, NULL);

When a child process terminates, the OS kernel will deliver a SIGCHLD signal, and the sigchld_handler function will be called.

Before forking a child process, the server will wait until the number of processes is at least one less than the maximum. Do this by:

while (g_num_procs >= MAX_PROCESSES) {
	int wstatus;
	wait(&wstatus);
	if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus))
		g_num_procs--;
}

int clientfd = Accept(serverfd, NULL, NULL);

g_num_procs++;
pid_t pid = fork();

Taking a closer look at the while statement, there is a period of time between checking the condition and waiting for the child to exit. Imagine if SIGCHLD is receieved during this peeriod of time since signals can be delivered at any time.

Assuming that sigchld_handler detects that a child process has exits, the call to wait is unecessary.

  • If MAX_PROCESSES is 1, then the server is going to be deadlocked!

There is another potential issue! The g_num_procs--; code is actually this under the hood:

int tmp = g_num_procs;
tmp = tmp - 1;
g_num_procs = tmp;
  • Note that tmp would be a register.

Consider what happens if the SIGCHLD signal is receieved after the initial value of g_num_procs is read, but before the updated value of tmp is stored back to g_num_procs.

  • The decrement of g_num_procs in sigchld_handler is lost, and the server no longer knows how many child processes are running!

These situations are called data race, and it is a (potential) bug where tow concurrentlt-executing paths access a shared variable, and at least one path writes to the variable.

  • Paths "race" to access shared data, outcome depends on which one "wins."
  • Data race is a specail case of a race condition, a situation where an execution outcome depends on unpredictable event sequencing.
  • Data races can cause data invariants to be violated (just like the two examples above would cause g_num_procs to inauccrately relflect the number of processes running).
  • Solution: use synchronization. Implement a protocol to avoid uncontrolled access to shared data.

Signal Blocking

Signal handler functions are a potential cause of data races because they execute asynchronously with respect to normal program execution. (The OS kernel could deliver a signal at any time).

sigprocmask allows programs to block and unblock a specific signal or signals.

  • Idea: block SIGCHLD whenever g_num_procs is being accessed by program code in order to prevent sigchld_handler from unexpectedly modifying g_num_procs.

To block or unblock SIGCHLD: toggle_sigchld function:

void toggle_sigchld(int how) {
	sigset_t sigs;
	sigemptyset(&sigs);
	sigaddset(&sigs, SIGCHLD);
	sigprocmask(how, &sigs, NULL);
}

Use to protect access to g_num_procs:

toggle_sigchld(SIG_BLOCK);
g_num_procs++;
toggle_sigchld(SIG_UNBLOCK);

Let's apply this to the server main loop:

while (1) {  
	wait_for_avail_proc();  
	int clientfd = accept connection from client toggle_sigchld(SIG_BLOCK);  
	g_num_procs++;  
	toggle_sigchld(SIG_UNBLOCK);  
	pid_t pid = fork();  
	if (pid < 0) {
		fatal("fork failed");
	} else if (pid == 0) { /* in child */
		server_chat_with_client(clientfd, webroot);
		close(clientfd);
		exit(0);
	}
	close(clientfd);
}

When a subprocess is forked, the child process inherits the parent process's file descriptors. So in the web server, the forked child process inherits clientfd, the socket connected to the client. This is convenient since we want the child process to handle the client's request.

Important: the parent process must close clientfd, otherwise the web socket will have a file descriptor leak. This is because its duplicated, so even if the child closes its file socket, the parent one would be left open.

  • OS kernel imposes limit on number of open files per process
  • Too many file descriptor open --> can't open any more files or sockets.

To limit the number of processes, the server calls wait_for_avail_proc before calling fork:

void wait_for_avail_proc(void) {
	toggle_sigchld(SIG_BLOCK);
	while (g_num_procs >= MAX_PROCESSES) {
		int wstatus;
		wait(&wstatus);
		if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) {
			g_num_procs--;
		}
	}
	toggle_sigchld(SIG_UNBLOCK);
}
  • Calls wait if too many processes are currently running.

Interrupted System Calls

When a program receieves a signal, it can interrupt the currently-executing system call.

This is why special handling is requred for accept system call to wait for connection from client:

int clientfd;
do {
	clientfd = accept(serverfd, NULL, NULL);
} while (clientfd < 0 && errno == EINTR);
if (clientfd < 0) {
	fatal("Error accepting client connection");
}
  • when the errno is EINTR, it indiciates that the system call was interrupted.

Because of the potential of signal handlers to introduce data races into the program, some library functions aren't safe to call from a signal handler. This is why its good to know man signal-safety on Linux.