Single-tasking vs. Multi-tasking Operating Systems

Single-tasking Operating Systems

DOSDOS was one of the most popular single-tasking (single-user) operating systems. Some of its "features":
Before processAfter process
Other notes:

Multi-tasking Operating Systems

UNIXUNIX Example

Operating System Concepts - 8th Edition Silberschatz, Galvin, Gagne ©2009  

Details

Operating System Concepts - 8th Edition Silberschatz, Galvin, Gagne ©2009  
MicrokernelsMicrokernels
  • By the way, Windows NTWindows NT is the basis for all NT operating systems (2000, Vista, 7, 8, 10, etc.) Previous versions of Windows (Windows 3.0, Windows 95, 98) were different (internally). And, yes, Windows NT really did start at version 3.1!
    Marketing NameInternal NameDate ReleasedBuild No.
    Windows NT 3.1 NT 3.1 July 1993 528
    Windows NT 3.5 NT 3.5 September 1994807
    Windows NT 3.51 NT 3.51May 1995 1057
    Windows NT 4 NT 4.0 July 1996 1381
    Windows 2000 NT 5.0 December 1999 2195
    Windows XP NT 5.1 August 2001 2600
    Windows Server 2003 NT 5.2 March 2003 3790
    Windows Vista NT 6.0 January 2007 6000
    Windows Server 2008 NT 6.0 March 2008 6001
    Windows 7 NT 6.1 October 2009 7600
    Windows Server 2008 R2NT 6.1 October 2009 7600
    Windows 8 NT 6.2 October 2012 9200
    Windows Server 2012 NT 6.2 September 20129200
    Windows 8.1 NT 6.3 October 2013 9600
    Windows Server 2012 R2 NT 6.3 October 20139600
    Windows 10 NT 10.0 July 29, 201510240-
    18985
    Windows Server 2016 NT 10.0 September 26, 201614393-
    16299
    Windows Server 2019 NT 10.0 October 2, 201817763

    Users, groups, and file privileges

    Modes of Operation Memory protection

    Time SharingTime Sharing

    Multiple tasks and memory System Services

    Operating System API

    Overview Executing a System Call POSIX (Portable Operating System Interface Standard) System Calls Advanced reading:

    These articles show all of the gory details of exactly what happens when you make a system call. I don't expect you to follow all of the details, but you should at least be able to appreciate the significant overhead that is involved when you call into the kernel. It is certainly more work than calling an "ordinary" function.

    Some Example System Calls

    Process management:

    CallDescription
    pid = fork(); Create a child process.
    pid = waitpid(pid, &statloc, options); Wait for a child to terminate.
    s = execve(name, argv, environp); Replace a process with another process.
    s = kill(pid, signal); Send a signal to a process.
    exit(status); Terminate a process and return status.
    File management:
    CallDescription
    fd = open(file, mode); Opens a file for reading/writing, etc.
    s = close(fd); Closes a file.
    n = read(fd, buffer, nbytes); Read bytes from a file into memory.
    n = write(fd, buffer, nbytes); Write bytes from memory to a file.
    Directory management:
    CallDescription
    s = mkdir(name, mode); Create a new directory.
    s = rmdir(name); Removes a directory.
    s = chdir(name); Change to another directory.
    s = unlink(name); Delete an existing file.
    Miscellaneous:
    CallDescription
    id = getuid(); Get the id of the current user.
    Comparing system calls using C code to assembly. You can really see the system calls when writing assembly code. These trivial programs simply read from standard in and write to standard out.

    C code: (rw.c)

    #include <stdio.h>  /* perror      */
    #include <unistd.h> /* read, write */
    
    #define BUFSIZE 1
    
    int main(void)
    {
        /* copy BUFSIZE bytes at a time from stdin to stdout */
      while (1)
      {
        unsigned char bytes[BUFSIZE];
        int count = read(0, bytes, BUFSIZE);
        if (count > 0)
          write(1, bytes, count);
        else
        {
          if (count == -1)
            perror("Read failed");
          break;
        }
      }
      return 0;
    }
    
    Assembly: (readwrite.asm)
    
    ;       readwrite < textfile
    ;
    ;  Build using these commands:
    ;       nasm -f elf64 -g -F dwarf readwrite.asm
    ;       ld -o readwrite readwrite.o
    ;
    SECTION .bss    
      BUFSIZE equ 64          ; how many bytes to read each time
      Buffer: resb BUFSIZE    ; buffer to read into
    
    SECTION .data                   
    SECTION .text                   
    global  _start                  
            
    ; Read from stdin
    ;  eax - SYS_read (3)
    ;  ebx - file descriptor (0 - stdin)
    ;  ecx - buffer to write into
    ;  edx - number of bytes to read
    _start: mov eax,3       ; SYS_read
            mov ebx,0       ; stdin is 0
            mov ecx,Buffer  ; address of Buffer
            mov edx,BUFSIZE ; number of bytes to read
            int 80h         ; make system call (traps to kernel)
            mov esi,eax     ; eax contains actual number of bytes read (save for later)
            cmp eax,0       ; if eax is 0 then the end of file was reached
            je Exit         ;     and we will exit the program
    
    ; Write to stdout
    ;  eax - SYS_write (4)
    ;  ebx - file descriptor (1 - stdout)
    ;  ecx - buffer to read from
    ;  edx - number of bytes to write
            mov eax,4       ; SYS_write
            mov ebx,1       ; stdout is 1
            mov ecx,Buffer  ; address of Buffer
            mov edx,esi     ; how many bytes to write (how many were read)
            int 80h         ; make system call (traps to kernel)
            jmp _start      ; read more bytes
    
    ; Exit the program
    Exit:
            mov eax,1       ; SYS_exit
            mov ebx,0       ; return value (to OS)
            int 80H         ; make system call
    
    The relevant system calls:
    #define __NR_exit    1
    #define __NR_read    3
    #define __NR_write   4
    
    #define SYS_exit     __NR_exit
    #define SYS_read     __NR_read
    #define SYS_write    __NR_write
    
    The system calls are defined in unistd.h and syscall.h


    The Win32 API

    The strace Program

    It is possible to "spy" on programs and see exactly what kinds of system calls are being made. This is trivial to do under Unix-based systems, such as Linux or macOS. (If strace isn't available on macOS, try dtrace instead. There's probably a wrapper script called dtruss that will probably work better.)

    This program (ptime.c) simply retrieves the current system time, formats it appropriately, and then prints it out on the screen.

    
    #include <stdio.h> /* printf                                */
    #include <time.h>  /* time, strftime, localtime, tm, time_t */
    
    int main(void)
    {
      struct tm *pt;
      char buf[256];
      time_t now;
     
        /* Get the current system time (number of seconds since January 1, 1970) */
      now = time(NULL);
     
        /* Format and print, Weekday, Month Day, Year HH:MM:SS AM/PM Timezone */
        /*    example:  Tuesday, May 22, 2018 5:23:36 PM PST                  */
      pt = localtime(&now);
      strftime(buf, sizeof(buf), "%A, %B %d, %Y %I:%M:%S %p %Z", pt);
      printf("%s\n", buf);
     
      return 0;  
    }
    
    The exact format is compiler/library dependent. Here is what it looks like from three different compilers:

    GNU gcc:

    Tuesday, May 22, 2018 06:01:15 PM PST
    
    Microsoft:
    Tuesday, May 22, 2018 06:01:25 PM Pacific Standard Time
    
    Borland:
    Tuesday, May 22, 2018 06:01:35 PM
    
    Let's spy on the program and see what's going on behind-the-scenes. Assuming that the name of the executable is ptime, we run strace on the program like this (under Linux):

    strace ./ptime > /dev/null
    
    or piping stderr through less:
    strace ./ptime 2>&1 > /dev/null | less
    
    and this is the output we see (errno and values)
    execve("./ptime", ["./ptime"], [/* 58 vars */]) = 0
    brk(0)                                  = 0x8051000
    access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
    mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb785d000
    access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
    open("/etc/ld.so.cache", O_RDONLY)      = 3
    fstat64(3, {st_mode=S_IFREG|0644, st_size=94651, ...}) = 0
    mmap2(NULL, 94651, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7845000
    close(3)                                = 0
    access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
    open("/lib/tls/i686/cmov/libc.so.6", O_RDONLY) = 3
    read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0000m\1\0004\0\0\0"..., 512) = 512
    fstat64(3, {st_mode=S_IFREG|0755, st_size=1405508, ...}) = 0
    mmap2(NULL, 1415592, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x194000
    mprotect(0x2e7000, 4096, PROT_NONE)     = 0
    mmap2(0x2e8000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x153) = 0x2e8000
    mmap2(0x2eb000, 10664, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x2eb000
    close(3)                                = 0
    mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7844000
    set_thread_area({entry_number:-1 -> 6, base_addr:0xb78446c0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
    mprotect(0x2e8000, 8192, PROT_READ)     = 0
    mprotect(0x8049000, 4096, PROT_READ)    = 0
    mprotect(0x192000, 4096, PROT_READ)     = 0
    munmap(0xb7845000, 94651)               = 0
    brk(0)                                  = 0x8051000
    brk(0x8072000)                          = 0x8072000
    open("/etc/localtime", O_RDONLY)        = 3
    fstat64(3, {st_mode=S_IFREG|0644, st_size=2819, ...}) = 0
    fstat64(3, {st_mode=S_IFREG|0644, st_size=2819, ...}) = 0
    mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb785c000
    read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\4\0\0\0\4\0\0\0\0"..., 4096) = 2819
    _llseek(3, -24, [2795], SEEK_CUR)       = 0
    read(3, "\nPST8PDT,M3.2.0,M11.1.0\n", 4096) = 24
    close(3)                                = 0
    munmap(0xb785c000, 4096)                = 0
    fstat64(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
    ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, 0xbf8804b0) = -1 ENOTTY (Inappropriate ioctl for device)
    mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb785c000
    write(1, "Tuesday, May 22, 2018 03:08:13 P"..., 38) = 38
    exit_group(0)                           = ?
    
    Some other useful options:
    OptionMeaning
    -c Only display summary information.
    -i Displays instruction pointer with each call.
    -r Displays relative timestamp (microseconds).
    -t Show time of day of each call.
    -tt Show time of day with microseconds for each call.
    -v Verbose. Show all parameters to system calls.
    -x Show non-ASCII in hex.
    -y Include filename with file descriptor.
    -a column Align return values on a specific column.
    -e trace=set Only show calls in set (.e.g trace=open,close).
    See the man page for strace for all of the options and details.

    A glimpse at the relations ship between FILE * and file handles:

    The FILE structure from GNU's compiler (version 4.4.3):

    struct _IO_FILE {
      int _flags;    /* High-order word is _IO_MAGIC; rest is flags. */
    
      /* The following pointers correspond to the C++ streambuf protocol. */
      /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
      char* _IO_read_ptr;   /* Current read pointer */
      char* _IO_read_end;   /* End of get area. */
      char* _IO_read_base;  /* Start of putback+get area. */
      char* _IO_write_base; /* Start of put area. */
      char* _IO_write_ptr;  /* Current put pointer. */
      char* _IO_write_end;  /* End of put area. */
      char* _IO_buf_base;   /* Start of reserve area. */
      char* _IO_buf_end;    /* End of reserve area. */
    
      struct _IO_marker *_markers;
    
      struct _IO_FILE *_chain;
    
      int _fileno;
      int _flags2;
    
      /* other fields removed */
    };
    typedef struct _IO_FILE FILE;
    
    The FILE structure from Microsoft's compiler (version 9.0):
    struct _iobuf {
            char *_ptr;
            int   _cnt;
            char *_base;
            int   _flag;
            int   _file;
            int   _charbuf;
            int   _bufsiz;
            char *_tmpfname;
            };
    typedef struct _iobuf FILE;
    
    Also from Microsoft's header file:
    #define stdin  (&__iob_func()[0])
    #define stdout (&__iob_func()[1])
    #define stderr (&__iob_func()[2])
    
    Incidentally, there is also a program called ltrace which traces library calls (user mode). Run it the same way:
    ltrace -n 2 ./ptime > /dev/null
    
    and this is the output we see: (Use -n X to indent calls, where X is the column to align at. The -S option shows system calls as well.)
    __libc_start_main(0x80484c4, 1, 0xbfd944c4, 0x8048550, 0x80485c0 
      time(NULL)                                                                                            = 1338329347
      localtime(0xbfd94314)                                                                                 = 0x006fc720
      strftime("Tuesday, May 22, 2018 03:09:07 P"..., 256, "%A, %B %d, %Y %I:%M:%S %p %Z", 0x006fc720)      = 37
      puts("Tuesday, May 22, 2018 03:09:07 P"...)                                                           = 38
    +++ exited (status 0) +++
    
    That's strange, where is the call to printf?

    This is a simple program (copy-read.c) that makes an exact copy of a file. It works like the copy command in Windows or the cp command in Linux. This program makes system calls to open, read, write, and close.

    #include <stdio.h>  /* printf, perror                       */
    #include <fcntl.h>  /* O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC */
    #include <unistd.h> /* open, close, read, write             */
    
    #define BUFSIZE 64
    
    int main(int argc, char **argv)
    {
      if (argc < 3)
      {
        printf("usage: copy {source} {destination}\n");
        return 1;
      }
      else
      {
        char *source = argv[1];      /* input file   */
        char *destination = argv[2]; /* output file  */
        int infile, outfile;         /* file handles */
        
          /* open source file for read-only */
        infile = open(source, O_RDONLY);
        if (infile == -1)
        {
          printf("Can't open %s for read\n", source);
          return 2;
        } 
        
          /* open destination file for write-only */
        outfile = open(destination, O_WRONLY | O_CREAT | O_TRUNC);
        if (outfile == -1)
        {
          printf("Can't open %s for write\n", destination);
          perror(destination);
          close(infile);
          return 3;
        }
        
          /* copy BUFSIZE bytes from source to destination */
        while (1)
        {
          unsigned char bytes[BUFSIZE];
    
          int count = read(infile, bytes, BUFSIZE);
          if (count > 0)
            write(outfile, bytes, count);
          else
          {
              /* error or EOF? */
            if (count == -1)
              perror(destination);
            break;
          }
        }
          /* clean up */
        close(infile);
        close(outfile);
        
        return 0;
      }
    }
    
    The larger the buffer, the more efficient the program is. These are the times (using the time command) when copying a 140 MB file. The buffer size ranged from 1 byte to 1 MB.
    1248163264
    real  3m2.412s
    user  0m7.690s
    sys  2m54.640s
    
    real  1m30.840s
    user   0m3.660s
    sys   1m27.150s
    
    real  0m48.064s
    user   0m2.050s
    sys   0m45.970s
    
    real  0m22.997s
    user   0m1.100s
    sys   0m21.860s
    
    real  0m11.541s
    user   0m0.630s
    sys   0m10.920s
    
    real  0m5.995s
    user  0m0.190s
    sys   0m5.800s
    
    real  0m2.998s
    user  0m0.150s
    sys   0m2.830s
    

    128256512102464K1M
    real  0m1.596s
    user  0m0.090s
    sys   0m1.500s
    
    real  0m0.935s
    user  0m0.010s
    sys   0m0.910s
    
    real  0m0.533s
    user  0m0.010s
    sys   0m0.520s
    
    real  0m0.375s
    user  0m0.000s
    sys   0m0.370s
    
    real  0m0.212s
    user  0m0.000s
    sys   0m0.210s
    
    real 0m0.202s
    user 0m0.000s
    sys  0m0.200s
    

    Here's the same program (copy-fread.c) using the C library functions fopen, fread, fwrite, and fclose. These library functions call the system functions.
    #include <stdio.h> /* printf, fopen, fread, fwrite, fclose */
    
    #define BUFSIZE 1
    
    int main(int argc, char **argv)
    {
      if (argc < 3)
      {
        printf("usage: copy {source} {destination}\n");
        return 1;
      }
      else
      {
        char *source = argv[1];      /* input file   */
        char *destination = argv[2]; /* output file  */
        FILE *infile, *outfile;      /* file handles */
        
          /* open source file for read-only */
        infile = fopen(source, "rb");
        if (!infile)
        {
          printf("Can't open %s for read\n", source);
          return 2;
        } 
        
          /* open destination file for write-only */
        outfile = fopen(destination, "wb");
        if (!outfile)
        {
          printf("Can't open %s for write\n", destination);
          fclose(infile);
          return 3;
        }
        
          /* copy BUFSIZE bytes at a time from source to destination (no error checking) */
        while (!feof(infile))
        {
          unsigned char bytes[BUFSIZE];
    
          int count = fread(bytes, sizeof(unsigned char), BUFSIZE, infile);
          if (count)
            fwrite(bytes, sizeof(unsigned char), count, outfile);
          else
            break;
        }
        
          /* clean up */
        fclose(infile);
        fclose(outfile);
      }
    }
    
    
    Looking at the times, there is obviously something very different between the two methods.

    Library calls (fopen, fread, etc.)

    12481632641282565121024512K
    real  0m6.930s
    user  0m6.480s
    sys   0m0.400s
    
    real  0m3.658s 
    user  0m3.280s
    sys   0m0.280s
    
    real  0m2.086s 
    user  0m1.660s
    sys   0m0.410s
    
    real  0m1.374s 
    user  0m1.010s
    sys   0m0.360s
    
    real  0m0.735s 
    user  0m0.450s
    sys   0m0.280s
    
    real  0m0.568s
    user  0m0.300s
    sys   0m0.270s
    
    real  0m0.445s
    user  0m0.110s
    sys   0m0.330s
    
    real  0m0.383s
    user  0m0.110s
    sys   0m0.270s
    
                  
                
                
    
                  
                
                
    
    real  0m0.348s
    user  0m0.050s
    sys   0m0.290s
    
    real  0m0.205s
    user  0m0.000s
    sys   0m0.200s
    

    System calls (open, read, etc.)
    1248163264128256512102464K1M
    real  3m2.412s
    user  0m7.690s
    sys  2m54.640s
    
    real  1m30.840s
    user   0m3.660s
    sys   1m27.150s
    
    real  0m48.064s
    user   0m2.050s
    sys   0m45.970s
    
    real  0m22.997s
    user   0m1.100s
    sys   0m21.860s
    
    real  0m11.541s
    user   0m0.630s
    sys   0m10.920s
    
    real  0m5.995s
    user  0m0.190s
    sys   0m5.800s
    
    real  0m2.998s
    user  0m0.150s
    sys   0m2.830s
    
    real  0m1.596s
    user  0m0.090s
    sys   0m1.500s
    
    real  0m0.935s
    user  0m0.010s
    sys   0m0.910s
    
    real  0m0.533s
    user  0m0.010s
    sys   0m0.520s
    
    real  0m0.375s
    user  0m0.000s
    sys   0m0.370s
    
    real  0m0.212s
    user  0m0.000s
    sys   0m0.210s
    
    real 0m0.202s
    user 0m0.000s
    sys  0m0.200s
    

    Other traces: The dumpit.exe program is 16,617 bytes (2 * 8,192 + 233 = 16,384 + 233) and the PDF is 950,639 bytes (29 * 32,768 + 367 = 950,272 + 367).

    Try it with other programs:

    strace ls
    strace ls -l
    strace cp
    
    More information on these programs: Interesting information: