Writing multithreaded programs under Linux

Assembly language, as any other structural language, allows programmers to write programs with one path of execution. There may be branches and loops, but there's always only one thing done at a time.

Thread allow running many independent paths, which will be executed in parallel. This gives big possibilities to programs doing more than one thing at a time (like reading from one file and writing processed data to the disk). Also network programs can have a gain, especially the servers. Using threads allows more than one client connecting at a time. Let's move on to the details then.

First I'm going to describe 3 functions from the C library (strictly speaking, from the pthreads library), which will allow us to manage threads.

  1. pthread_create - creation of a new thread.

    This function accepts 4 parameters. Starting from the left (last put on the stack), these are:


  2. pthread_exit - exit the current thread

    This function finishes the current thread. The value given as its only parameter (address of data) can be used by threads joined (pthread_join) to this thread. After closing all the threads, the program finishes with error code 0.

  3. pthread_yield - giving away the processor to other threads or processes

    Of course, the operating system assigns processor time to thread, but by calling this function we can tell it to shorten the time for this thread, thus give more to others. This is useful when the current thread has temporarily finished work (like when there's no data to process). This function has no parameters.

Below is a short program, which will show you how this works. This program's job is to display the first string once in the main function and display the second string 5 times in the thread function.


(skip the program)
; Example multithreaded program in assembly
;
; Author: Bogdan D., bogdandr (at) op.pl
;
; assembly:
; nasm -O999 -f elf -o watki.o watki.asm
; gcc -o watki watki.o -lpthread

section	.text
global	main

; external function declarations
extern	pthread_create
extern	pthread_exit

main:

	mov	eax, 4
	mov	ebx, 1
	mov	ecx, napis1
	mov	edx, napis1_dl
	int	80h			; print string one

	push	dword 0			; extra data
	push	dword watek		; address of function to run
	push	dword 0			; attributes
	push	dword id_watku		; where to put the ID
	call	pthread_create		; create new thread

; Do not exit the program with sys_exit (EAX=1), because
; this would stop all the threads of the program. We close only the main
; thread instead.
	push	dword 0
	call	pthread_exit		; stop the current thread

watek:

	mov	dword [t1+timespec.tv_nsec], 0
	mov	dword [t1+timespec.tv_sec], 5		; 5 seconds

	mov	esi, 5		; the second string will be printed 5 times
.petla:
	mov     eax, 162		; sys_nanosleep
	mov     ebx, t1			; address of a structure telling
					; how much time to wait
	mov     ecx, 0
	int     80h			; make a pause...

	mov	eax, 4
	mov	ebx, 1
	mov	ecx, napis2
	mov	edx, napis2_dl
	int	80h			; print string two

	dec	esi
	jnz	.petla			; loop if ESI != 0

	push	dword 0
	call	pthread_exit		; exit current thread

section .data

napis1		db	"Main function.", 10
napis1_dl	equ	$ - napis1

napis2		db	"thread.", 10
napis2_dl	equ	$ - napis2

struc timespec
	.tv_sec:	resd 1
	.tv_nsec:	resd 1
endstruc

t1 		istruc timespec

id_watku	dd	0	; variable which will get the new thread's ID

But having threads in a program isn't just the good stuff. The biggest problem in multithreaded programs is thread synchronizing.

Why synchronize? To make sure that the program will not run into trouble when two or more threads read and write the same global variable (like a data buffer).

What should you do to make the reader thread process the data only when another thread writes the data? There are a few possibilities:

As you can see, writing multithreaded programs is not that hard, and it's worth learning. Usually the gain is bigger (writing one function for each task) than the work (synchronization).


Multithreading using int 80h

You don't have to use any libraries to write multithreaded programs. The needed mechanisms are also provided by the system interface - int 80h.

I'm going to use the system function sys_fork (number 2) here. Its only parameter is an address of a structure containing the register values for the new process, but this parameter is optional and can be a zero. The fork function returns a negative value in case of error, zero in the child process and a positive value (the child's process ID) in the parent process. The child process starts execution right after the call to fork, thus the parent and the child start executing exactly the same code. You can direct the processes to new paths using the return value in EAX.

Here's a short example in FASM syntax:

format ELF executable
entry _start
segment executable

_start:
	mov	eax, 2		; the fork function number
	xor	ebx, ebx
	int	80h		; call it

	cmp	eax, 0
	jl	.koniec		; EAX < 0 means error

	; the below instructions will be executed by both the parent and the child:

	cmp	eax, 0
	jg	.rodzic		; EAX > 0 means this is the parent process

	; here neither EAX < 0, nor EAX > 0, so EAX=0, so
	; this is the child process
	; the below code (printing and waiting) will be executed only
	; by the child process

	mov	dword [t1.tv_nsec], 0
	mov	dword [t1.tv_sec], 5	; this many seconds will the pause
					; between printing last

.petla:
	mov	eax, 4		; file writing function
	mov	ebx, 1		; standard output
	mov	ecx, napis2	; what to write
	mov	edx, napis2_dl	; length of the string
	int	80h

	mov     eax, 162	; sys_nanosleep
	mov     ebx, t1		; wait this much
	mov     ecx, 0		; an address of a second timespec structure
				; can be put here
	int     80h		; do the pause...

	jmp	.petla		; and start all over again....

	; the below code (printing and exit) will be executed only
	; by the child process
.rodzic:

	mov	eax, 4		; file writing function
	mov	ebx, 1		; standard output
	mov	ecx, napis1	; what to write
	mov	edx, napis1_dl	; length of the string
	int	80h

.koniec:
	mov	eax, 1		; exit the program
	xor	ebx, ebx
	int	80h

segment readable writeable

napis1		db	"Parent", 10
napis1_dl	=	$ - napis1
napis2		db	"Child", 10
napis2_dl	=	$ - napis1

struc timespec			; timespec structure definition
				; (only as a data type)
{
	.tv_sec:	rd 1
	.tv_nsec:	rd 1
}

t1 timespec		; t1 is a variable representing the whole structure


On-line contents (access key 2)
Helpers for people with disabilities (access key 0)