C++ System Programming Cookbook

5 (1 reviews total)
By Onorato Vaticone
    What do you get with a Packt Subscription?

  • Instant access to this title and 7,500+ eBooks & Videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Free Chapter
    Getting Started with System Programming
About this book

C++ is the preferred language for system programming due to its efficient low-level computation, data abstraction, and object-oriented features. System programming is about designing and writing computer programs that interact closely with the underlying operating system and allow computer hardware to interface with the programmer and the user. The C++ System Programming Cookbook will serve as a reference for developers who want to have ready-to-use solutions for the essential aspects of system programming using the latest C++ standards wherever possible.

This C++ book starts out by giving you an overview of system programming and refreshing your C++ knowledge. Moving ahead, you will learn how to deal with threads and processes, before going on to discover recipes for how to manage memory. The concluding chapters will then help you understand how processes communicate and how to interact with the console (console I/O). Finally, you will learn how to deal with time interfaces, signals, and CPU scheduling.

By the end of the book, you will become adept at developing robust systems applications using C++.

Publication date:
February 2020


Getting Started with System Programming

In this chapter, you will be introduced to the foundations on which the entire book is framed. You will learn (or refresh your knowledge of) how Linux is designed, and you will also learn about the shell, users and groups, process IDs, and thread IDs to be able to use a Linux system proficiently and get prepared for the next chapters. Furthermore, you will also learn how to develop a simple hello world program, and find out about its makefile, and also how to execute and debug it. Another important aspect of this chapter is to learn how Linux deals with errors, from both a shell and a source code point of view. This foundational knowledge is important to understand other advanced topics in the following chapters. You can safely skip this and the next chapters if this refresher is not needed.

This chapter will cover the following recipes: 

  • Learning the Linux fundamentals – architecture
  • Learning the Linux fundamentals – shell
  • Learning the Linux fundamentals – users
  • Using a makefile to compile and link a program
  • Using the GNU Project Debugger (GDB) to debug a program
  • Learning the Linux fundamentals – processes and threads
  • Handling a Linux bash error
  • Handling Linux code error

Technical requirements

In order to let you try the programs immediately, we've set up a Docker image that has all the tools and libraries we'll need throughout the book. This is based on Ubuntu 19.04.

In order to set this up, follow these steps:

  1. Download and install the Docker Engine from www.docker.com.
  2. Pull the image from Docker Hub: docker pull kasperondocker/system_programming_cookbook:latest
  3. The image should now be available. Type in the following command to view the image: docker images
  4. You should have at least this image now: kasperondocker/system_programming_cookbook
  5. Run the Docker image with an interactive shell, with the help of the following command: docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash. 
  6. The shell on the running container is now available. Run root@39a5a8934370/# cd /BOOK/ to get all the programs developed, by chapters.

The --cap-add sys_ptrace argument is needed to allow GDB in the Docker container to set breakpoints, which, by default, Docker does not allow.


Learning the Linux fundamentals - architecture

Linux is a clone of the Unix operating system, developed by Linus Torvalds in the early '90s. It is a multiuser, multitasking operating system that runs on a wide variety of platforms. The Linux kernel has a monolithic architecture for performance reasons. This means that it is self-contained in one binary, and all its services run in kernel space. This was one of the most controversial topics at the beginning. Andy Tanenbaum (professor at the Vrije Universiteit, Amsterdam) argued against its monolithic system, saying: This is a giant step back into the 1970s. He also argued against its portability, saying: LINUX is tied fairly closely to the 80 x 86. Not the way to go. In the minix user group, there still is the thread of full chat involving Torvalds, Tanenbaum, and others.

The following diagram shows the main Linux building blocks:

Let's describe the layers we see in the diagram:

  • On the top layer, there are user applications, processes, compilers, and tools. This layer (which runs in a user space) communicates with the Linux kernel (which runs in kernel space) through system calls.
  • System libraries: These are a set of functions through which an application can interact with the kernel. 
  • Kernel: This component contains the core of the Linux system. Among other things, it has the scheduler, networking, memory management, and filesystems.
  • Kernel modules: These contain pieces of kernel code that still run in kernel space but are fully dynamic (in the sense that they can be loaded and unloaded with the running system). They typically contain device drivers, kernel code that is specific to a particular hardware module implementing a protocol, and so on. One huge advantage of the kernel modules is that users can load them without rebuilding the kernel.

GNU is a recursive acronym that stands for GNU is Not Unix. GNU is an operating system that is free software. Note the term operating system here. Indeed, GNU used alone is meant to represent a full set of tools, software, and kernel parts that an operating system needs. The GNU operating system kernel is called the Hurd. As the Hurd was not production-ready, GNU typically uses the Linux kernel, and this combination is called the GNU/Linux operating system.

So, what are the GNU components on a GNU/Linux operating system? Packages* such as the GNU Compiler Collection (GCC), the GNU C library, GDB, the GNU Bash shell, and the GNU Network Object Model Environment (GNOME) desktop environment, to mention just a few. Richard Stallman and the Free Software Foundation (FSF)—of which Stallman is the founder—authored the free software definition to help respect users' freedom. Free software is considered any package that grants users the following four types of freedoms (so-called essential freedomshttps://isocpp.org/std/the-standard):

  1. The freedom to run the program as you wish, for any purpose (Freedom 0).
  2. The freedom to study how the program works and to change it, so it does your computing as you wish (Freedom 1). Access to the source code is a precondition for this.
  3. The freedom to redistribute copies so that you can help others (Freedom 2).
  4. The freedom to distribute copies of your modified versions to others (Freedom 3). By doing this, you can give the whole community a chance to benefit from your changes. Access to the source code is a precondition for this.

The concrete instantiation of these principles is in the GNU/GPL license, which FSF authored. All of the GNU packages are released under the GNU/GPL license.

How to do it...

Linux has a pretty standard folder structure across the distributions, so knowing this would allow you to easily find programs and install them in the correct place. Let's have a look at it as follows:

  1. Open a Terminal on the Docker image.
  2. Type the command ls -l /.

How it works...

The output of the command will contain the following folders:

As you can see this folder structure is pretty organized and consistent across all the distributions. Under the hood, the Linux filesystem is quite modular and flexible. A user application can interact with the GNU C library (which provides interfaces such as open, read, write, and close) or the Linux system call directly. The system call interface, in this case, talks to the Virtual Filesystem (often referred to as the VFS). The VFS is the abstraction on top of the concrete filesystem implementations (for example, ext3, Journaled File System (JFS), and more). This architecture, as we can imagine, gives a high level of flexibility.


Learning the Linux fundamentals - shell

A shell is a command interpreter that receives commands in an input, redirects them to GNU/Linux, and returns back the output. It is the most common interface between a user and GNU/Linux. There are different shell programs available. The most used ones are Bash shell (part of the GNU Project), tcsh shell, ksh shell, and zsh shell (this is basically an extended Bash shell).

Why would you need a shell? A user needs a shell if they need to interact with the operating system through the command line. In this recipe, we'll show some of the most common shell commands. Quite often, the terms shell and Terminal are used interchangeably, even though, strictly speaking, they are not exactly the same thing.

How to do it...

In this section, we will learn the basic commands to run on the shell—for example, to find a file, grep a text into a file, copy, and delete:

  1. Opening a shell: Depending on the GNU/Linux distribution, opening a new shell command has different shortcuts. On Ubuntu, press Ctrl + Alt + T, or press Alt + F2, then type gnome-terminal.
  2. Closing a shell: To close Terminal, just type exit and press Enter.
  3. The find command: This is used to search files in a directory hierarchy. In its simplest form, it appears like this: 
find . -name file

It supports wildcards, too:

$ find /usr/local "python*"
  1. The grep command prints the lines by matching a pattern: 
 $ grep "text" filename

grep also supports recursive search: 

 $ grep "text" -R /usr/share
  1. Pipe commands: Commands running on the shell can be concatenated, to make the output of one command the input for another. The concatenation is done with the | (pipe) operator:
$ ls -l | grep filename
  1. Editing a file: The most two common tools to edit a file on Linux are vi and emacs (if you're not interested in editing the file, cat filename will print the file to the standard output). While the first is inherited by the Unix operating system, the latter is part of the GNU Project. This book will extensively use vi:
 $ vi filename

Next, we will look at shell commands related to file manipulation.

  1. This is the command to remove files:
$ rm filename
  1. This is the command to remove directories:
$ rm -r directoryName
  1. This is the command to clone a file:
$ cp file1 file2
  1. This is the command to clone a folder:
$ cp -r folder1 folder2  
  1. This is the command to clone a folder using a relative and absolute path:
$ cp -r /usr/local/folder1 relative/folder2

The next section will describe these commands.

How it works...

Let's have a look at the commands discussed in the How to do it... section, in detail:

  1. The first command searches (.) from the current folder and can contain absolute paths (for example, /usr/local) or relative paths (for example, tmp/binaries). For example, here, -name is the file to search.
  2. The second command searches from the /usr/local folder any file or folder that starts with pythonThe find command offers huge flexibility and a wide variety of options. For more information, refer to man page through the man find command.
  3. The grep command searches and prints any line that contains the word text in the filename file.
  4. The grep recursive search command searches and prints any line that contains the word text in any file recursively from the /usr/share folder. 
  5. Pipe command (|): The output of the first command is shown in the following screenshot. A list of all the files and directories is passed as input to the second command (grep), which will be used to grep the filename:

Now, let's look at the commands that perform actions such as editing a file, and adding/removing files and directories.

Editing a file:

  • The vi command will open the filename in edit mode, assuming the current user has writing permissions on it (we will discuss permissions in more detail later).
    The following is a short summary of the most used commands in vi:
    • Shift + : (that is, the Shift key + colon) to switch in edit mode.
    • Shift + :i to insert.
    • Shift + :a to append.
    • Shift + :q! to quit the current session without saving.
    • Shift + :wq to save and quit the current session.
    • Shift + :set nu to show the line numbers on the file.
    • Shift + :23 (Enter) goes at line 23.
    • Press the (Esc) key to switch to command mode.
    • . to repeat the last command.
    • cw to change the word, or do this by pointing the cursor at the beginning of the word.
    • dd to remove the current line.
    • yy to copy the current line. If a number N is selected before the yy command, the N line will be copied.
    • p to paste the copied line with the yy command.
    • u to undo.

Adding and removing files and directories:

  1. The first command removes the file named filename
  2. The second command removes directoryName and its content, recursively.
  3. The third command creates file2, which is an exact copy of file1
  4. The fourth command creates folder2 as a clone of folder1

There is a common pattern in the execution of the commands shown in this recipe. They are listed as follows:

  1. The user types a command and hits Enter.
  2. The command is interpreted by Linux.
  3. Linux interacts with its different parts (memory management, networking, filesystem, and more) to execute the command. This happens in kernel space.
  4. The results are returned to the user.

There's more...

This recipe showed some of the most recurrent commands. Mastering all the options, even just for the most common shell commands, is tricky, and that is why man pages were created. They contain a solid and clear reference for the Linux user.

See also

Chapter 8Dealing with Console I/O and Files, will go deeper into console I/O and file management.


Learning the Linux fundamentals - users

Linux is a multiuser and multitasking operating system, so basic user administration skills are a must. This recipe will show you how permissions for files and directories are structured, how to add and remove a user, how to change a user's password, and how to assign a user to a group.

How to do it...

The following series of steps shows useful commands for basic user administration activities:

  1. Creating a user: Having one user configured for each individual using Linux is not just a best practice, it is also recommended. Creating a user is quite simple:
root@90f5b4545a54:~# adduser spacex --ingroup developers
Adding user `spacex' ...
Adding new user `spacex' (1001) with group `developers' ...
Creating home directory `/home/spacex' ...
Copying files from `/etc/skel' ...
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for spacex
Enter the new value, or press ENTER for the default
Full Name []: Onorato
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n] Y

The spacex user has been created and assigned to the existing developers group. To switch to the newly created user, log in using the new user's credentials:

root@90f5b4545a54:~# login spacex
Welcome to Ubuntu 19.04 (GNU/Linux 4.9.125-linuxkit x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
To restore this content, you can run the 'unminimize' command.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
  1. Updating a user's password: Periodically, the password must be changed. Here is the command to do this:
spacex@90f5b4545a54:~$ passwd
Changing password for spacex.
Current password:
New password:
Retype new password:
passwd: password updated successfully
  1. Assigning a user to a group: As shown, a user can be assigned to a group when created. Alternatively, a user can be assigned to a group at any time, by running the following command:
root@90f5b4545a54:~# usermod -a -G testers spacex
here spacex is added to the testers group
  1. Removing a user: Likewise, removing a user is pretty simple:
root@90f5b4545a54:~# userdel -r spacex
userdel: spacex mail spool (/var/mail/spacex) not found

The -r option indicates to remove the spacex home directory and mail spool.

  1. Now, let's have a look at the final command, which shows a list of the groups to which the current user (spacex) belongs:
spacex@90f5b4545a54:~$ groups
developers testers

As you can see, the spacex user belongs to the developers and testers groups.

How it works...

In step 1, we used the adduser command to add the spacex user and, contextually, added the user to the developers group.

Step 2 shows how to change the password of the current user. To change the password, the previous password must be provided. It is a good practice to change the password periodically. 

If we want to assign a user to a group, it can be done with the usermod command. In step 3, we have added the spacex user to the testers group. The -a and -G parameters just indicate that the new groups (-G) will be appended to the current groups (-aof the user. That is, the spacex user will be assigned to the testers group, which will be contextually created. The groups command, in the same step, shows which groups the current user belongs to. If you only want to create a group, then groupadd group-name is the command you need.

Step 4 shows how to remove a user with the userdel command, passing the -r parameter. This parameter ensures that all the files of the user we're removing will be deleted.

There's more...

On a Linux filesystem, each file and directory has a set of information defining who can do what. The mechanism is simple, as well as powerful. The operations allowed on a file (or directory) are read, write, and execute (r, w, and x, respectively). These operations can be done by the owner of the file or directory, by a group of users, or by all users. Linux represents this information with Owner: rwx; Group: rwx; All Users: rwxor, more simply: rwx-rwx-rwx (9 in total). Actually, Linux has one more flag on top of these ones that represents the type of file. It can be a folder (d), a symbolic link to another file (l), a regular file (-), a named pipe (p), a socket (s), a character device file (c), and a block device (b). Typical permissions for a file look like this:

root@90f5b4545a54:/# ls -l
-rwxr-xr-x 1 root root 13 May 8 20:11 conf.json

Let's see this in detail:

  • Reading from the left-hand side, the first character, -, informs us that conf.json is a regular file.
  • The next three characters are about the current user, rwx. The user has full read (r), write (w), and execution (x) permissions over the file.
  • The next three chars are about the group to which the user belongs, r-x. All the users belonging to the group can read and execute the file, but cannot modify it (w is not selected, marked as -).
  • The last three characters are about all the other users, r-x. All other users can just read and execute the file (r and x are marked, but w is not).

The owner (or the root user) can change the permissions of the file. The easiest way to achieve this is through the chmod command:

 $ chmod g+w conf.json 

Here, we're asking the Linux kernel to add the write permission (w) to the group user type (g). The types of users are as follows: u (for user), o (for others), a (for all), and g (for group), and the permissions flag can be x, w, and r, as explained previously. chmod can also accept an integer:

 $ chmod 751 conf.json 

There is a binary-to-decimal conversion on permission flags for each group type, for example:
wxr: 111 = 7
w-r: 101 = 5
--r: 001 = 1

It could be a little cryptic at the beginning, but it is very practical and handy for everyday use.

See also

The man pages are an infinite resource of information and should be the first thing you look at. Commands such as man groups, man userdelor man adduser will help with this.


Using a makefile to compile and link a program

A makefile is a file that describes the relationship among the sources of a program used by the make utility to build (compile and link) the target goal (executable, shared object, and more). Makefiles are really important as they help to keep sources organized and easy to maintain. A program, to become executable, must be compiled and linked with other libraries. GCC is the most widely used collection of compilers. The two compilers used in the C and C++ world are GCC and g++ (for the C and C++ programs, respectively). This book will use g++.

How to do it...

This section will show how a makefile is written, to compile and run a simple C++ program. We'll develop a simple program, and create its makefile to learn its rules:

  1. Let's start by developing the program by opening the hello.cpp file: 
$vi hello.cpp
  1. Type in the following code (refer to the Learning the Linux fundamentals - shell recipe to review the vi commands):
#include <iostream>
int main()
std::cout << "Hello World!" << std::endl;
return 0;
  1. Save and exit: in vi, from the command mode, type :wq, which means write and quit. The :x command has the same effect.
  2. From the shell, create a new file called Makefile:
$ vi Makefile
  1. Type in the following code:
CC = g++
all: hello
hello: hello.o
${CC} -o hello hello.o
hello.o: hello.cpp
${CC} -c hello.cpp
rm hello.o hello

Although this is a typical Hello World! program, it is useful to show how a makefile is structured.

How it works...

Simply, a makefile consists of a set of rules. A rule consists of a target, a list of prerequisites, and a command.

In the first step, we opened the file (hello.cpp) and typed the program listed in step 2. Likewise, we opened another file, Makefile, in the same folder of the hello.cpp program, and typed the specific makefile commands. Let's now dive into the makefile internals. A typical makefile has the following content:

  1. The first rule consists of a target called all, and a prerequisite called hello. There is no command for this rule.
  2. The second rule consists of a target called hello. It has a prerequisite on hello.o and a command to link: g++.
  3. The third rule has a target called hello.o, a prerequisite on hello.cpp, and a command to compile: g++ -c hello.cpp.
  4. The last rule has a clean target with a command to remove all the hello and hello.o executables. This forces the recompilation of the files.
  5. For any rule, if any of the source files change, then the command defined is executed.

We're now able to compile the program using the makefile we created:

$ make

We're also able to execute the program, whose output is as follows:

The process of generating a binary executable from a source file includes the phase of compilation and linking, which here is compressed inside a single command; it'll be like this in most cases. In general, a large system code base relies on more sophisticated mechanisms but the steps are still the same: source file editing, compilation, and linking.

There's more...

This simple example just showed us the very basic concepts of a makefile and its make command. There is much more to it than that. Here are a few examples:

  1. Use of macros: A makefile allows the use of macros, which can be seen as variables. These can be used to organize the makefile to be more modular, for example:
    • A macro for all the dynamic libraries used in the program: LIBS = -lxyz -labc
    • A macro for the compiler itself (in case you want to change to another compiler): COMPILER = GCC
    • Reference these macros over all the makefile: $(CC). This gives us the freedom to make changes in just one place.
  2. By just typing make on a shell, the first rule defined in the makefile will run. In our case, the first rule is all. If we changed the makefile by putting clean as a first rule, running make without parameters would execute the clean rule. In general, you'll always pass some parameters—for example, make clean.

Using GDB to debug a program

Debugging is the process of identifying and removing errors from software systems. The GNU/Linux operating system has a standard de facto tool (that is, not part of any standard, but used by almost anybody in the Linux world) called GDB. The GDB version installed on this book's Docker is version 8.2.91. Of course, there are graphical tools that can use GDB under the hood, but GDB on Linux is the way to go for its reliability, simplicity, and speed. In this recipe, we will debug the software we've written in the previous recipe.

How to do it...

In order to use some of the GDB commands, we need to modify the previous program and add some variables in it:

  1. Open a shell and modify the hello.cpp file by typing in the following code:
 #include <iostream>
int main()
int x = 10;
x += 2;
std::cout << "Hello World! x = " << x << std::endl;
return 0;

This is a very simple program: take a variable, add 2 to it, and print the result.

  1. Let's make sure that the program is compiled by typing the following command:
root@bffd758254f8:~/Chapter1# make
g++ -c hello.cpp
g++ -o hello hello.o
  1. Now that we have the executable, we will debug it. From the command line, type gdb hello:
root@bffd758254f8:~/Chapter1# gdb hello
GNU gdb (Ubuntu
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
Find the GDB manual and other documentation resources online at:
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from hello...
(No debugging symbols found in hello)
  1. As you can see, the last line says (No debugging symbols found in hello). GDB doesn't have to debug symbols to debug the program, so we have to communicate to the compiler that the debug symbols are to be included during the compilation. We have to quit the current session; to do this, type q (Enter]. Then, edit the makefile, and add the -g option to the g++ compiler section (the hello.o target):
CC = g++
all: hello
hello: hello.o
${CC} -o hello hello.o
hello.o: hello.cpp
$(CC) -c -g hello.cpp
rm hello.o hello
  1. Let's run it again, but, first, we have to rebuild the application with the make command:
root@bcec6ff72b3c:/BOOK/chapter1# gdb hello
GNU gdb (Ubuntu
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
Find the GDB manual and other documentation resources online at:
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from hello...
(No debugging symbols found in hello)

We're ready to debug it. A debug session typically includes setting breakpoints, watching the content of variables, setting watchpoints, and many others. The next section will show the most common debug commands.

How it works...

In the previous section, we have seen the steps necessary to create a program and a makefile. In this section, we'll learn how to debug the Hello World! program we developed.

Let's start by visualizing the code we're going to debug. We do this by running the l command (short for list):

(gdb) l
1 #include <iostream>
2 int main()
3 {
4 int x = 10;
5 x += 2;
6 std::cout << "Hello World! x = " << x << std::endl;
7 return 0;
8 }

We have to set a breakpoint. To set a breakpoint, we run the b 5 command. This sets a breakpoint to the code line number 5 in the current module:

(gdb) b 5
Breakpoint 1 at 0x1169: file hello.cpp, line 5.

It's time to run the program now. To run a program, we type the r command. This runs the hello program we started with GDB:

(gdb) r
Starting program: /root/Chapter1/hello

Once started, GDB will automatically stop at any breakpoint hit by the process flow. In this case, the process runs, and then stops at line 5 of the hello.cpp file:

Breakpoint 1, main () at hello.cpp:5
5 x += 2;

To proceed step by step, we run the n command (that is, step over) on GDB. This executes the current visualized line of code. A similar command is s (step into). If the current command is a function, it steps into the function:

(gdb) n
6 std::cout << "Hello World! x = " << x << std::endl;
the 'n' command (short for next) execute one line. Now we may want to check the content of the variable x after the increment:

If we need to know the content of a variable, we run the p command (short for print), which prints the content of a variable. In this case, as expected, x = 12 gets printed:

(gdb) p x
$1 = 12

Now, let's run the program until the end (or until the next breakpoint, if set). This is done with the c command (short for continue):

(gdb) c 
Hello World! x = 12
[Inferior 1 (process 101) exited normally]

GDB really acts as an interpreter by letting the programmer step the program line by line. This helps the developer to troubleshoot problems, see the content of variables at runtime, change the status of variables, and more.

There's more...

GDB has a lot of very useful commands. In the following chapters, GDB will be explored more. There are four more commands to show here:

  1. s: Short for step. If called on a method, it steps into it.
  2. bt: Short for backtrace. Prints the call stack.
  3. q: Short for quit. Use to exit GDB.
  4. d: Short for delete. It removes a breakpoint. For example, d 1 removes the first breakpoint set.
The main page of the GNU GDB Project can be found here: https://www.gnu.org/software/gdbMore detailed information can be found on the man dbg man pages and online. You can also refer to Using GDB: A Guide to the GNU Source-Level Debugger, by Richard M. Stallman and Roland H. Pesch.

Learning the Linux fundamentals - processes and threads

Processes and threads are the execution units of any operating system. In this recipe, you'll learn how to deal with processes and threads on GNU/Linux on the command line. A process is a running instance of a program with a well-defined set of resources such as files, processor state, and threads of execution allocated to it.

A process in Linux is defined by the task_struct structure defined in the sched.h header file. On the other hand, a thread is defined by the thread_info structure in the thread_info.h header file. A thread is one possible flow of execution of the main process. A process has at least one thread (the main thread). All the threads of a process run concurrently on a system.

One aspect to keep in mind on Linux is that it doesn't differentiate between processes and threads. A thread is just like a process that shares some resources with some other processes. For this reason, in Linux, threads are often referred to as a lightweight process (LWP).

How to do it...

In this section, we'll learn, step by step, all the most common commands to control processes and threads on a GNU/Linux distribution:

  1. The ps command shows the processes, attributes, and other parameters in the current system:
root@5fd725701f0f:/# ps u
root 1 0.0 0.1 4184 3396 pts/0 Ss 17:20 0:00 bash
root 18 0.0 0.1 5832 2856 pts/0 R+ 17:22 0:00 ps u
  1. Another way to get info on a process (and its threads) is to look in the /process/PID folder. This folder contains all the process info, threads of the process (in the form of subfolders with process identifiers (PIDs)), memory, and much more:
root@e9ebbdbe3899:/# ps aux
root 1 0.0 0.1 4184 3344 pts/0 Ss 16:24 0:00 bash
root 149 0.0 0.1 4184 3348 pts/1 Ss 17:40 0:00 bash
root 172 85.0 0.0 5832 1708 pts/0 R+ 18:02 0:04 ./hello
root 173 0.0 0.1 5832 2804 pts/1 R+ 18:02 0:00 ps aux
root@e9ebbdbe3899:/# ll /proc/172/
total 0
dr-xr-xr-x 9 root root 0 May 12 18:02 ./
dr-xr-xr-x 200 root root 0 May 12 16:24 ../
dr-xr-xr-x 2 root root 0 May 12 18:02 attr/
-rw-r--r-- 1 root root 0 May 12 18:02 autogroup
-r-------- 1 root root 0 May 12 18:02 auxv
-r--r--r-- 1 root root 0 May 12 18:02 cgroup
--w------- 1 root root 0 May 12 18:02 clear_refs
-r--r--r-- 1 root root 0 May 12 18:02 cmdline
-rw-r--r-- 1 root root 0 May 12 18:02 comm
-rw-r--r-- 1 root root 0 May 12 18:02 coredump_filter
-r--r--r-- 1 root root 0 May 12 18:02 cpuset
lrwxrwxrwx 1 root root 0 May 12 18:02 cwd -> /root/Chapter1/
-r-------- 1 root root 0 May 12 18:02 environ
lrwxrwxrwx 1 root root 0 May 12 18:02 exe -> /root/Chapter1/hello*
dr-x------ 2 root root 0 May 12 18:02 fd/
dr-x------ 2 root root 0 May 12 18:02 fdinfo/
-rw-r--r-- 1 root root 0 May 12 18:02 gid_map
-r-------- 1 root root 0 May 12 18:02 io
-r--r--r-- 1 root root 0 May 12 18:02 limits
  1. A process can be killed, too. Technically, killing a process means stopping its execution:
root@5fd725701f0f:/# kill -9 PID

This command sends the kill signal (9) to the process identified with the PID. Other signals can be sent to processes—for example, HUP (hangup) and INT (interrupt).

How it works...

In step 1 for each process, we can see the following:

  • The user to whom the process belongs
  • The PID
  • The percentage of CPU and memory in a specific moment
  • When the process started, and its running time
  • The command used to run the process

Through the ps aux command, we can grab the PID of the hello process, which is 172. We can now look into the /proc/172 folder.

Processes and threads are building blocks of an operating system. In this recipe, we've seen how to interact with the kernel on the command line to get info on processes through a command (for example, ps), and by looking into a specific folder that Linux updates as the process runs. Again, every time we invoke a command (to get info on a process, in this case), the command must enter in kernel space to get valid and updated info on it.

There's more...

The ps command has many more parameters than the basic one seen in this recipe. A complete list is available on its Linux man page, man ps

A more advanced and interactive command to consider as an alternative to ps is the top command, man top


Handling a Linux bash error

We've seen that one way to interact with the Linux kernel is through the shell, by invoking commands. A command can fail, as we can imagine, and a way to communicate a failure is to return a non-negative integer value. 0, in most cases, means success. This recipe will show you how to deal with error handling on the shell.

How to do it...

This section will show you how to get errors directly from the shell and via a script, which is a fundamental aspect of script development:

  1. First, run the following command:
root@e9ebbdbe3899:/# cp file file2
cp: cannot stat 'file': No such file or directory
root@e9ebbdbe3899:/# echo $?
  1. Create a new file called first_script.sh and type in the following code:
cat does_not_exists.txt
if [ $? -eq 0 ]
echo "All good, does_not_exist.txt exists!"
exit 0
echo "does_not_exist.txt really DOES NOT exists!!" >&2
exit 11
  1. Save the file, and exit (:wq or :x).
  2. Give execution permission (the x flag) to the current user for the first_script.sh file:
root@e9ebbdbe3899:~# chmod u+x first_script.sh

These steps are detailed in the next section.

How it works...

In step 1, the cp command failed, as file and file2 don't exist. By querying echo $?, we get the error code; in this case, it is 1. This is particularly useful when writing bash scripts where we might need to check for a particular condition.

In step 2, the script just lists the does_not_exist.txt file and reads the error code returned. If all goes fine, it prints an acknowledgment message and returns 0. Otherwise, it returns the error code 11.

By running the script, we get the output as follows:

Here, we notice a couple of things:

  • We logged our error string.
  • The error code is the one we had set in the script.

Under the hood, every time a command is invoked, it enters into kernel space. The command is executed, and a return status is sent back to the user in the form of an integer. It's really important to consider this return status, as we might have a command that apparently succeeded (no output) but eventually failed (returns code different from 0).

There's more...

One important aspect of the return status of the commands is that it can be used to (conditionally) run the next command. Two important operators are used for this purpose: && (AND) and || (OR).

In the two commands here, the second is run if—and only if—the first succeeds (the && operator). file.txt is removed if it is copied to the project folder:

cp file.txt ~/projects && rm -f file.txt

Let's have a look at a second example:

cp file.txt ~/projects || echo 'copy failed!'

In the preceding example, the second command is run only if the first fails (the || operator). copy failed! is printed if the copy fails.

In this recipe, we just showed that commands can be combined on a shell script to create a more complex command, and by controlling the error code, we can control the flow of execution. Man pages are a great resource as they contain all the commands and error codes (for example, man cp and man cat).


Handling Linux code error

This recipe represents the second side of the coin in the topic of error handling: error handling at a source-code level. Linux exposes its kernel features through commands, as well as through a programming API. In this recipe, we'll see how to deal with error codes and errno through a C program, to open a file.

How to do it...

In this section, we'll see how to get the error from a system call in a C program. To do this, we'll create a program to open a non-existent file and show the details of the error returned by Linux:

  1. Create a new file: open_file.c
  2. Edit the following code in the newly created file:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
int fileDesc = open("myFile.txt", O_RDONLY);
if (fileDesc == -1)
fprintf(stderr, "Cannot open myFile.txt .. error: %d\n",
fprintf(stderr, "errno code = %d\n", errno);
fprintf(stderr, "errno meaningn = %s\n", strerror(errno));
  1. Save the file and exit (:x).
  2. Compile the code: gcc open_file.c
  3. The preceding compilation (without parameters) will produce a binary file called a.out (which is the default name on the Linux and Unix operating systems).

How it works...

The program listed tries to open a file in reading mode. Errors are printed on standard error, through the fprintf command. By running it, the output will be as follows:

There are a couple of considerations to highlight. The program is developed by strictly following the man page of the open system call (man 2 open):

If successful, open() returns a non-negative integer, termed a
file descriptor. It
returns -1 on failure, and sets errno to indicate the error

The developer (us, in this case) checked that the file descriptor was -1 (confirmed by fprintf) to print errno too (with code 2). What does errno 2 mean? strerror is useful exactly for this scope, to translate from errno (which is cryptic) to something the programmer (or the user) would understand.

There's more...

In Chapter 2Revisiting C++, we'll see how C++ helps programmers by providing higher-level mechanisms, and easy-to-write and more concise code. Even if we try to minimize the interaction with the kernel API directly, in favor of the use of the C++11-14-17 higher-level mechanism, there will be cases where we'll need to check the error status. In those cases, you are invited to pay attention to error management.

About the Author
  • Onorato Vaticone

    Onorato Vaticone is a software engineer with over 18 years of experience. A C++ expert, he has deep, system-level programming experience. An Agile coach and XP advocate, TDD and Simple Design are his everyday tools. He has worked on real-time systems (defense and energy transmission) with C++. During this time, he learned to write multiplatform code. Early in his career, he realized that a form of agility was needed. He holds an MSc in cloud computing and a BSc in computer engineering and software. He finds learning how things work under the hood to be fascinating!

    Browse publications by this author
Latest Reviews (1 reviews total)
the content is what I need for programming on a linux system, it really helps my knowledge of the kernel and its implementation.
Recommended For You
C++ System Programming Cookbook
Unlock this book and the full library FREE for 7 days
Start now