Multitasking
By default, a program can only do one thing at a time. There are many cases when we want to do multiple tasks in parallel.
- πͺ Splitting long tasks into subtasks executed in parallel
- π Running multiple tasks in parallel
- π Using another program to do a task
Multitasking can be done using Processes or Threads.
There are a few related topics:
- Signals: communication with a process/between processes
- Pipes: communication between processes
- ...
Processes
A process is both a program and its environment.
- file descriptors (file opened, position of the cursor...)
- variables and environment variables
- ...
When executing ./a.out
, the code is executed by the main
process.
#include <unistd.h> // getpid, getppid
int main() { // "pid_t" is an alias of "int"
pid_t pid = getpid();
pid_t ppid = getppid(); // -1 if none
}
A process can duplicate itself using fork
. The return value is 0 inside the newly created process, and >0 in the original
#include <unistd.h> // fork, getpid
int main() {
pid_t pid = fork(); // todo: handle pid == -1 (error)
if (pid == 0) {
// executed by the newly created process
printf("Child[%d].\n", getpid());
} else {
// executed by the original process
printf("Parent[%d].\n", getpid());
}
exit(0); // executed by both (not inside the if)
}
Communication between processes
Wait/Exit code
Usually, we want to know when our processes are done.
#include <unistd.h> // fork
#include <sys/wait.h> // wait
int main() {
// create 3 processes
for (int i = 0; i < 3; ++i) {
if (fork() == 0) {
// do something for your parent
exit(i); // SUCCESS
}
}
// wait for each process to die
for (int i = 0; i < 3; ++i) {
wait(NULL);
}
// do something with what your children did
exit(0);
}
You can also use while
as wait return -1
if there is no one to wait for.
while(wait(NULL) != -1);
If you want to get the exit code (ex: to check if a child failed its task)
// β‘οΈ replace "wait(NULL);" with:
int status;
pid_t child_pid = wait(&status); // store it if you need it
if (WIFEXITED(status)) { // if exited
int exit_code = WEXITSTATUS(status); // get code
// ...
}
β οΈ Actually, wait
is blocking the parent until a signal is received. This could be another signal other than the exit one:
-
WIFEXITED(status)
: process was killed -
WIFSIGNALED(status)
: process was killed (manually) -
WCOREDUMP(status)
: process was killed (core dump) -
WIFSTOPPED(status)
: process was stopped -
WIFCONTINUED(status)
: process was restarted
And you want some convenient functions:
-
WEXITSTATUS(status)
: return the exit code if applicable -
WTERMSIG(status)
: return the terminating signal code -
WSTOPSIG(status)
: return the stopping signal code
waitpid
: more versatile wait
int waitpid(int pid, int *status, int options);
// example
waitpid(-1, &status, 0);
-
pid
: $-1$ (any), $0$ (any child in the group), $>0$ (a specific process). Aside from $-1$, $-n$ is the same as $n$. -
options
: can be used to ignore some signals/...
Signals
When using CTRL+C, you're sending a signal to a program.
-
code
: from 1 to 31 included -
function
: for instance, for 9 (kill):exit(130)
.
You can change how your code will respond to a signal.
#include <signal.h>
void handler(int signum) {
// do something
}
int main() {
// sig_t signal(int code, void (*handler)(int));
if (SIG_ERR == signal(9, handler)) {
perror("Using custom handler for 9 failed");
exit(1);
} else {} // ok
}
You can use kill
(the function/the command) to send a signal
// if pid = 0 then all processes in our group
// if pid = -1 then all processes
// else send a signal to the one with <pid>
int kill(pid_t pid, int signal_code);
You can wait for a signal using pause
or sleep
#include <unistd.h>
sleep(1000); // wake up by itself after 1s
pause(); // won't wake up by itself
Pipes
A pipe
is a read/write stream in which both processes can exchange. To understand pipe, you must first understand file descriptors which in short, are numbers representing a file (see System calls).
The function pipe
is creating two file descriptors
-
tab[0]
: to read using the system callread
-
tab[1]
: to write using the system callwrite
Example: sending "Hello World" to the original
#include <unistd.h>
#include <wait.h>
int main() {
int length = 11 + 1; // \0
char buf[length];
int tab[2];
pipe(tab);
switch (fork()) { // add -1
case 0: // the child write Hello World
write(tab[1], "Hello World", length);
break;
default:
wait(NULL); // wait for the child to write
read(tab[0], &buf, length); // read
// ...
break;
}
close(tab[0]);
close(tab[1]);
}
You can also use named pipes. These are created and accessible from the file system (ex: using ls
).
// int mkfifo(const char* name, mode_t mode);
int fd = mkfifo("filename", 0777);
Process replacement
The code executed by a process can be replaced, for instance, if you want to run a command or another executable.
We are using variants of the system call "exec
". If the process is successfully replaced, then the code after the exec is NEVER called, otherwise, the exec
functions will return -1
.
exec[...]([...]);
perror("exec failed"); // β only called if "exec" fails
execl
: take a list of arguments
// signature πΊοΈ
int execl(const char* ref, const char * args, ..., NULL);
// example π₯
execl("/bin/ls", "ls", "-la", ".", NULL);
- π The first argument is the path to the executable
- π The second argument is the process name shown using
ps
- π The following arguments are the parameters passed to the executable. The last argument must be
NULL
.
execle
: also take an environment
π The last argument is an array of environment variables. The last value must be NULL
.
// signature πΊοΈ
int execle(const char* ref, const char * args, ..., NULL, char* const envp[]);
// example π₯
char *envp[] = {"TARGET=.", NULL};
execle("/bin/ls", "ls", "-la", "$TARGET", NULL, envp);
execlp
: use the PATH to find the executable
π The first argument is now a name that we will look for in the PATH, instead of an absolute path to the executable.
// signature πΊοΈ
int execle(const char* name, const char * args, ..., NULL);
// example π₯
execle("ls", "ls", "-la", ".", NULL);
execv
: use an array instead of a list
There are also variants: execvp
and execvpe
... like for execl
.
// signature πΊοΈ
int execv(const char* ref, const char * argv[]);
// example π₯
char *args[] = {"ls", "-l", NULL};
execv("/bin/ls", args);
Threads
Threads, also called "light processes" are similar to processes, aside from the fact that they share a part of the parent environment πͺΈ.
It means that a thread can modify a variable in the parent, and if the parent reads the variable, they will see the updated value. This causes new problems related to concurrency π₯.
To compile, you must use -pthread
("modern" -lpthread
) with gcc
.
#include <pthread.h> // gcc [...] -pthread
In a nutshell, a thread is running a function, and dies. The function is taking and returning something of type void*
. This is because we can store any pointer such as an int*
in a void*
.
void *my_function(void *arg){
int v = *((int*) arg);
printf("%d\n", v);
pthread_exit(NULL); // dies π - exit value
}
We use pthread_create
to create and run a thread. The function takes
-
thread
: an empty variable to store the newly created thread -
envr
: an environment, can beNULL
-
function
: a function taking avoid*
and returning avoid*
-
arg
: an argument passed to your function
pthread_t thread1;
int arg = 5;
// int pthread_create(thread, envr, function, (void*) arg);
pthread_create(&thread1, NULL, my_function, (void*) &arg);
Similarly to processes, we want to wait for the thread to die, meaning it finished its task. We can do that using pthread_join
:
// int pthread_join(pthread_t thread, void **code);
pthread_join(thread1, NULL);
π "code" is an empty pointer of the same type as the return value.
Threads: concurrency
Synchronisation using mutex
A mutex
is a mechanism to only allow one person at a time to execute some code, usually to safely modify a variable.
- π Before executing the "unsafe" code, we try to lock the
mutex
- β³ If we can't, we have to wait until the
mutex
is unlocked - π After executing the "unsafe" code, we unlock the
mutex
First, initialize the mutex:
// global variable πΊοΈ
// version 1: using a MACRO
pthread_mutex_t mutex_var = PTHREAD_MUTEX_INITIALIZER;
// version 2: using functions
pthread_mutex_t mutex_var;
pthread_mutex_init(&mutex_var, NULL);
pthread_mutex_destroy(&mutex_var);
In the example below, we only want to allow one person to do the "unsafe operation" which is increasing global_variable
. Without a mutex, global_variable
make take unexpected values due to concurrent modifications.
int global_variable = 5;
void *my_function(void *arg){
// π try to lock
pthread_mutex_lock(&mutex_var);
// π° unsafe operation
variable_globale++;
// π unlock
pthread_mutex_unlock(&mutex_var);
}
Load balancing using semaphores
If we have limited resources, we may want to allow up to n
threads to access the limited resource at the same time. We can do that using semaphores π§π«§.
π Semaphores can be used for synchronization too, and more...
#include <semaphore.h>
First, initialize the semaphore with the maximum number n
:
// global variable πΊοΈ
sem_t semaphore;
// in the "main" function
sem_init(&semaphore, 0, n);
// ...
sem_destroy(&semaphore);
The semaphore can be viewed as a box with n
tickets. The function wait
will either pick a ticket if there is one, or wait for one. Once the operation is done, the function post
is used to put back the ticket in the box, allowing someone else to work π.
void *my_function(void *arg){
sem_wait(&semaphore);
// operation...
sem_post(&semaphore);
}
Threads: condition variables
Conditions variables are used to stop executing a thread until a condition is true. You will need a mutex, and a condition variable:
// global variables πΊοΈ
pthread_mutex_t mutex = ...;
pthread_cond_t cond;
int n = 0; // current number of threads using the resource
void *my_function(void *arg){
pthread_mutex_lock(&mutex);
while(n == 5) // a_condition
pthread_cond_wait(&cond, &mutex);
n++;
// some code...
n--;
// π Notify others to check the condition again
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
}
Breakdown π
Since each thread may read/edit variables used in a_condition
, we need a mutex.
The condition is something that we define, such as n == 5
. If the condition is true, pthread_cond_wait
will unlock the mutex, and wait for pthread_cond_broadcast
.
It means we can't use the resource and wait β³.
Otherwise, we can access the resource, and work on it. Once we are done, we need to tell others using pthread_cond_broadcast
π’.
pthread_cond_broadcast
is a method that we call to notify any waiting thread that they should wake up as a_condition
may have changed.
π The condition always changes, but not all threads may be able to access the resource once they wake up; some will go back to sleep.
π» To-do π»
Stuff that I found, but never read/used yet.
-
FILE* stream = fopen(FIFO_PATH, "r+");
-
dup/dup2/dup3
- adding exercises from ens