Unit 1 1.1 Introduction To Operating System 1.1.1 Definition of Operating System
Unit 1 1.1 Introduction To Operating System 1.1.1 Definition of Operating System
Unit 1 1.1 Introduction To Operating System 1.1.1 Definition of Operating System
Note : Please note that Unix will make an exact copy of the
parent's address space and give it to the child. Therefore, the
parent and child processes have separate address spaces.
Example:
*Calculate a number of times hello is printed.*
#include <stdio.h>
#include <sys/types.h>
int main()
{
fork();
fork();
fork();
printf("hello\n");
return 0;
}
*Solutions:*
Number of times hello printed is equal to number of processes
created. Total Number of Processes = 2^n where n is the number
of fork system calls. So here n = 3, 2^3 = 8.
Interprocess Communication
Processes within a system may be independent or
cooperating
Cooperating process can affect or be affected by other
processes, including sharing data
Reasons for cooperating processes:
Information sharing
Computation speedup
Modularity
Convenience
Cooperating processes need interprocess communication
(IPC)
Two models of IPC
Shared memory
Message passing
Communications Models
Cooperating Processes
Independent process cannot affect or be affected by the execution
of another process
Cooperating process can affect or be affected by the execution of
another process
Motivation
This is particularly true when one of the tasks may block, and it is
desired to allow the other tasks to proceed without blocking.
For example in a word processor, a background thread may check
spelling and grammar while a foreground thread processes user
input ( keystrokes ), while yet a third thread loads images from the
hard drive, and a fourth does periodic automatic backups of the
file being edited.
1.5 Benefits
Multicore Programming
A recent trend in computer architecture is to produce chips with
multiple cores, or CPUs on a single chip.
A multi-threaded application running on a traditional single-core
chip would have to interleave the threads, as shown in Figure 3.
On a multi-core chip, however, the threads could be spread across
the available cores, allowing true parallel processing, as shown in
Figure 4.
Types of Parallelism
In theory there are two different ways to parallelize the workload:
Data parallelism divides the data up amongst multiple
cores ( threads ), and performs the same task on each
subset of the data. For example dividing a large image up
into pieces and performing the same digital image
processing on each piece on different cores.
Task parallelism divides the different tasks to be
performed among the different cores and performs them
simultaneously.
In practice no program is ever divided up solely by one or the other
of these, but instead by some sort of hybrid combination.
1.6 Multithreading Models
There are two types of threads to be managed in a modern system:
User threads and kernel threads.
User threads are supported above the kernel, without
kernel support. These are the threads that application
programmers would put into their programs.
Kernel threads are supported within the kernel of the OS
itself. All modern OSes support kernel level threads,
allowing the kernel to perform multiple simultaneous tasks
and/or to service multiple kernel system calls
simultaneously.
In a specific implementation, the user threads must be mapped to
kernel threads, using one of the following strategies.
1) Many-To-One Model
In the many-to-one model, many user-level threads are all mapped
onto a single kernel thread.
Thread management is handled by the thread library in user space,
which is very efficient.
However, if a blocking system call is made, then the entire process
blocks, even if the other user threads would otherwise be able to
continue.
Because a single kernel thread can operate only on a single CPU,
the many-to-one model does not allow individual processes to be
split across multiple CPUs.
Green threads for Solaris and GNU Portable Threads implement
the many-to-one model in the past, but few systems continue to do
so today.
Many-to-One Model
2) One-To-One Model
The one-to-one model creates a separate kernel thread to handle
each user thread.
One-to-one model overcomes the problems listed above involving
blocking system calls and the splitting of processes across
multiple CPUs.
However the overhead of managing the one-to-one model is more
significant, involving more overhead and slowing down the
system.
Most implementations of this model place a limit on how many
threads can be created.
Linux and Windows from 95 to XP implement the one-to-one
model for threads.
One-to-One Model
3) Many-To-Many Model
The many-to-many model multiplexes any number of user threads
onto an equal or smaller number of kernel threads, combining the
best features of the one-to-one and many-to-one models.
Users have no restrictions on the number of threads created.
Blocking kernel system calls do not block the entire process.
Processes can be split across multiple processors.
Individual processes may be allocated variable numbers of kernel
threads, depending on the number of CPUs present and other
factors.
Many-to-Many Model
One popular variation of the many-to-many model is the two-tier
model, which allows either many-to-many or one-to-one
operation.
IRIX, HP-UX, and Tru64 UNIX use the two-tier model, as did
Solaris prior to Solaris 9.
Two-tier Model
Pthreads
The POSIX standard ( IEEE 1003.1c ) defines the
specification for pThreads, not the implementation.
pThreads are available on Solaris, Linux, Mac OSX,
Tru64, and via public domain shareware for Windows.
Global variables are shared amongst all threads.
One thread can wait for the others to rejoin before
continuing.
pThreads begin execution in a specified function, in this example
the runner( ) function:
Implicit Threading ( Optional )
Shifts the burden of addressing the programming challenges
outlined above from the application programmer to the compiler
and run-time libraries.
Signal Handling
Q: When a multi-threaded process receives a signal, to what thread
should that signal be delivered?
A: There are four major options:
Deliver the signal to the thread to which the signal applies.
Deliver the signal to every thread in the process.
Deliver the signal to certain threads in the process.
Assign a specific thread to receive all signals in a process.
The best choice may depend on which specific signal is involved.
UNIX allows individual threads to indicate which signals they are
accepting and which they are ignoring. However the signal can
only be delivered to one thread, which is generally the first thread
that is accepting that particular signal.
UNIX provides two separate system calls, kill( pid, signal ) and
pthread_kill( tid, signal ), for delivering signals to processes or
specific threads respectively.
Windows does not support signals, but they can be emulated using
Asynchronous Procedure Calls ( APCs ). APCs are delivered to
specific threads, not processes.
1.10 Thread Cancellation
Threads that are no longer needed may be cancelled by another
thread in one of two ways:
Asynchronous Cancellation cancels the thread immediately.
Deferred Cancellation sets a flag indicating the thread should
cancel itself when it is convenient. It is then up to the cancelled
thread to check this flag periodically and exit nicely when it sees
the flag set.
( Shared ) resource allocation and inter-thread data transfers can
be problematic with asynchronous cancellation.
Example program:
#include<stdio.h>
int main()
{
int fd;
if((fd=open(“file.dat”))==-1)
{
perror(“cannot open the file.dat”);
exit(0);
}
else
printf(“\n FILE OPENED SUCCESSSFULLY”);
return 0;
}
2. Read
This function fetches a fixed size block of data from a file referenced
by a given file descriptor
#include <sys/types.h>
#include <unistd.h>
ssize_t read (int fdesc ,void* buf, size_t size);
Return Value : Number of bytes read successfully & -1 on failure or
reaching End-of-file
Arguments:
fdesc : The file descriptor of a file from where the contents are to be
read.
Buf: The buffer where the read contents are to be stored
Size: The number of bytes to be read.
Example program:
#include<stdio.h>
main()
{
char b[20];
int fd,xr;
if((fd=open(“write”,0))==-1)
{
printf(“cannot open file”);
exit(1);
}
do
{
xr=read(fd,b,20);
b[xr]=’\0’;
printf(“%s”,b);
}
while(xr==20);
close(fd);
}
3. Write
The write function puts a fixed size block of data to a file referenced
by a file descriptor
#include <sys/types.h>
#include <unistd.h>
ssize_t read (int fdesc ,void* buf, size_t size);
Return Value : Number of bytes written successfully & -1 on failure
or reaching End-of-file
Arguments:
fdesc : The file descriptor of a file to which the contents are to be
written.
Buf: The buffer where the contents to be written are to stored
Size: The number of bytes to be read.
Example program:
#include<stdio.h>
main(int ac,char*av[])
{
int fd;
int i=1;
char*sep=” “;
if(ac<1)
{
printf(“\n INSUFFICIENT ARGUMENTS");
exit(1);
}
if((fd=open(“file.dat”,0660))==-1)
{
printf(“\n CANNOT CREATE THE FILE”);
exit(1);
}
while(i<ac)
{
write(fd,av[i],(unsigned)strlen(av[i]));
write(fd,sep,(unsigned)strlen(sep));
i++;
}
close(fd);
}
4. Close
Disconnects a file from a process. Close function will deallocate
system resources
#include <unistd.h>
int close (int fdesc);
Return Value : 0 on success & -1 on failure
Arguments:
fdesc : The file descriptor which is to be closed
5. Link
The link function creates a new link for existing file i.e make a new
name for a file.
Prototype :
#include <unistd.h>
int link (const char* cur_link ,const char* new_link)
Return Value : 0 on success & -1 on failure
Arguments:
cur_link : Path name of the existing file
new_link: New path name
6. unlink
Delete a name and possibly the file it refers to
#include <unistd.h>
int unlink (const char* cur_link );
Return Value : 0 on success & -1 on failure
Arguments:
cur_link : Path name of the file to delete
Example program: link & unlink
#include <unistd.h>
int main(int argc, char* argv[]) {
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
void printFileProperties(struct stat stats);
int main()
{
char path[100];
struct stat stats;
printf("Enter source file path: ");
scanf("%s", path);
if (stat(path, &stats) == 0)
{
printFileProperties(stats);
}
else
{
printf("Unable to get file properties.\n");
printf("Please check whether '%s' file exists.\n", path);
}
return 0;
}
void printFileProperties(struct stat stats)
{
struct tm dt;
printf("\nFile access: ");
if (stats.st_mode & R_OK)
printf("read ");
if (stats.st_mode & W_OK)
printf("write ");
if (stats.st_mode & X_OK)
printf("execute");
printf("\nFile size: %d", stats.st_size);
dt = *(gmtime(&stats.st_ctime));
printf("\nCreated on: %d-%d-%d %d:%d:%d", dt.tm_mday,
dt.tm_mon, dt.tm_year + 1900,
dt.tm_hour, dt.tm_min, dt.tm_sec);
dt = *(gmtime(&stats.st_mtime));
printf("\nModified on: %d-%d-%d %d:%d:%d", dt.tm_mday,
dt.tm_mon, dt.tm_year + 1900,
dt.tm_hour, dt.tm_min, dt.tm_sec);
}
8. fcntl file locking
The function helps to query or set access control flags and the close-
on-exec flag of any file descriptor. It can also be used for locking and
unlocking of files as follows:
Syntax:
#include<fcntl.h>
int fcntl (int fdesc, int cmd_flag, …);
Return Value : 0 on success & -1 on failure
Arguments:
fdesc: file descriptor of the file
Cmd_flag : command or action to be performed
Cmd_flag
F_SETLK - Sets a file lock. Do not block if this cannot succeed
immediately
F_SETLKW - Sets a file lock and blocks the calling process until
the lock is acquired
F_GETLK - Queries as to which process locked a specified region
of a file
The l_len specifies the size of a locked region beginning from the start
address defined by l_whence and l_start. If l_len is 0 then the length of
the locked is imposed on the maximum size and also as it extends. It
cannot have a –ve value.
When fcntl is called, the variable contains the region of the file locked
and the ID of the process that owns the locked region. This is returned
via the l_pid field of the variable.
Example program:
#include <unistd.h>
#include<fcntl.h>
int main ( ) {
int fd;
struct flock lock;
fd=open(“foo.txt”,O_RDONLY);
lock.l_type=F_RDLCK;
lock.l_whence=0;
lock.l_start=10;
lock.l_len=15;
fcntl(fd,F_SETLK,&lock);
fvar.l_type = F_UNLCK;
fvar.l_whence = SEEK_SET;
fvar.l_start = 0;
fvar.l_len = 0;
if((fcntl(fd,F_UNLCK,&fvar))==-1)
{
perror("fcntl");
exit(0);
}
printf("Unlocked\n");
close(fd);
}
9. Opendir
This opens the file for read-only Readdir. he opendir() function
opens a directory stream corresponding to the directory name, and
returns a pointer to the directory stream. The stream is positioned
at the first entry in the directory.
DIR*opendir (const char* path_name);
Return value: a pointer to the directory streamon success and on
error, NULL is returned,
Argument:
Path_name: path name of directory to be opened
10.Readdir
The readdir() function returns a pointer to a dirent structure
representing the next directory entry in the directory stream pointed to
by dirp.
Dirent* readdir(DIR* dir_fdesc);
returns: NULL on reaching the end of the directory stream or if an
error occurred.
Arguments:
dir_fdesc: value is the DIR* return value from an opendir call.
In the glibc implementation, the dirent structure is defined as
follows:
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not
supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename
*/
};
11.closedir
It terminates the connection between the dir_fdesc handler and a
directory file.
int closedir (DIR* dir_fdesc);
returns: 0 on Success and -1 on Failure.
Arguments:
dir_fdesc: value is the DIR* return value from an opendir call.
#include<stdio.h>
#include<dirent.h>
main(int argc, char **argv)
{
DIR *dp;
struct dirent *link;
dp=opendir(argv[1]);
printf(“\n contents of the directory %s are
\n”, argv[1]);
while((link=readdir(dp))!=0)
printf(“%s”,link->d_name);
closedir(dp);
}
12.Fork
fork() creates a new process by duplicating the calling process. The
new process is referred to as the child process. The calling process is
referred to as the parent process.
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
On success, the PID of the child process is returned in the parent, and 0
is returned in the child. On failure, -1 is returned in the parent, no
child process is created
13.exec
The exec() family of functions replaces the current process image with
a new process image.
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, ... /* (char *)
NULL */);
int execlp(const char *file, const char *arg, ... /* (char *)
NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char
*) NULL, char * const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const
envp[]);
Return Value: The exec() functions return only if an error has
occurred. The return value is -1
The functions can be grouped based on the letters following the "exec"
prefix.
l - execl(), execlp(), execle()
The const char *arg and subsequent ellipses can be thought of as arg0,
arg1, ..., argn. Together they describe a list of one or more pointers to
null-terminated strings that represent the argument list available to the
executed program. The first argument, by convention, should point to
the filename associated with the file being executed. The list of
arguments must be terminated by a null pointer, and, since these are
variadic functions, this pointer must be cast (char *) NULL.
v - execv(), execvp(), execvpe()
The char *const argv[] argument is an array of pointers to null-
terminated strings that represent the argument list available to the new
program. The first argument, by convention, should point to the
filename associated with the file being executed. The array of pointers
must be terminated by a null pointer.
e - execle(), execvpe()
The environment of the caller is specified via the argument envp. The
envp argument is an array of pointers to null-terminated strings and
must be terminated by a null pointer. All other exec() functions (which
do not include 'e' in the suffix) take the environment for the new
process image from the external variable environ in the calling process.
p - execlp(), execvp(), execvpe()
These functions duplicate the actions of the shell in searching for an
executable file if the specified filename does not contain a slash (/)
character.
14.wait
Parent wait for state changes in a child of the calling process, and
obtain information about the child whose state has changed. A
state change is considered to be: the child terminated; the child was
stopped by a signal; or the child was resumed by a signal. In the case
of a terminated child, performing a wait allows the system to release
the resources associated with the child; if a wait is not performed, then
the terminated child remains in a "zombie" state
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
Return value: on success, returns the process ID of the terminated
child; on error, -1 is returned
Argument
wstatus: state of terminated child
WIFEXITED(wstatus)
returns true if the child terminated normally, that is, by
calling exit(3) or _exit(2), or by returning from main().
WEXITSTATUS(wstatus)
returns the exit status of the child. This consists of the
least significant 8 bits of the status argument that the child
specified in a call to exit(3) or _exit(2) or as the argument
for a return statement in main(). This macro should be
employed only if WIFEXITED returned true.
WIFSIGNALED(wstatus)
returns true if the child process was terminated by a signal.
WTERMSIG(wstatus)
returns the number of the signal that caused the child process
to terminate. This macro should be employed only if
WIFSIG‐
NALED returned true.
WCOREDUMP(wstatus)
returns true if the child produced a core dump (see core(5)).
This macro should be employed only if WIFSIGNALED
returned
true. This macro is not specified in POSIX.1-2001 and is not
avail‐
able on some UNIX implementations (e.g., AIX, SunOS).
There‐
fore, enclose its use inside #ifdef WCOREDUMP ... #endif.
WIFSTOPPED(wstatus)
returns true if the child process was stopped by delivery of a
signal; this is possible only if the call was done using WUN‐
TRACED or when the child is being traced (see ptrace(2)).
WSTOPSIG(wstatus)
returns the number of the signal which caused the child to
stop. This macro should be employed only if WIFSTOPPED
returned true.
WIFCONTINUED(wstatus)
(since Linux 2.6.10) returns true if the child process was
resumed by delivery of SIGCONT.
15.Process ID
Every process has a unique process ID, a non negative integer.
Following functions can be used to print various IDs of process
#include <unistd.h>
#include <sys/types.h>
pid_t getpid (void);
pid_t getppid (void);
uid_t getuid (void);
uid_t geteuid (void);
gid_t getgid (void);
gid_t getegid (void);
16.exit
exit performs certain cleanup processing and then returns to kernel
#include <stdlib.h>
void _exit (int status)
void exit (int status)
execl("./encrypt","encry",argv[1],argv[2],NULL)
;//second arg is just a reference name
}
else{
printf("Inside parent process ID
=%d\n",getpid());
wait(&res);
if(WIFEXITED(res)==1)
{
printf("Terminates normally\n");
}
else{
printf("AbNormal termination");
exit(0);
}
}
}
Linux Case Study
Design Principles
Module-management
Loading a module requires more than just loading its binary
contents into kernel memory. The system must also make sure that
any references the module makes to kernel symbols or entry points
are updated to point to the correct locations in the kernel’s address
space. Linux deals with this reference updating by splitting the job
of module loading into two separate sections: the management of
sections of module code in kernel memory and the handling of
symbols that modules are allowed to reference.
The loading of the module is performed in two stages. First, the
moduleloader utility asks the kernel to reserve a continuous area
of virtual kernel memory for the module. The kernel returns the
address of the memory allocated, and the loader utility can use this
address to relocate the module’s machine code to the correct
loading address. A second system call then passes the module,
plus any symbol table that the new module wants to export, to the
kernel. The final module-management component is the module
requester. The kernel defines a communication interface to which
a module-management program can connect.
Driver Registration
Once a module is loaded, it remains no more than an isolated
region of memory until it lets the rest of the kernel know what new
functionality it provides. The kernel maintains dynamic tables of
all known drivers and provides a set of routines to allow drivers to
be added to or removed from these tables at any time.
A device driver might want to register two separate mechanisms
for accessing the device. Registration tables include, among
others, the following items:
Device drivers. These drivers include character devices
(such as printers, terminals, and mice), block devices
(including all disk drives), and network interface devices.
File systems. The file system may be anything that
implements Linux’s virtual file system calling routines.
Network protocols. A module may implement an entire
networking protocol, such as TCP or simply a new set of
packet-filtering rules for a network firewall.
Binary format. This format specifies a way of recognizing,
loading, and executing a new type of executable file.
Conflict Resolution
Linux provides a central conflict-resolution mechanism to help
arbitrate access to certain hardware resources. Its aims are as
follows:
To prevent modules from clashing over access to hardware
resources.
To prevent auto probes—device-driver probes that auto-
detect device configuration—from interfering with
existing device drivers.
To resolve conflicts among multiple drivers trying to
access the same hardware—as, for example, when both the
parallel printer driver and the parallel line IP (PLIP)
network driver try to talk to the parallel port
To these ends, the kernel maintains lists of allocated hardware
resources. The PC has a limited number of possible I/O ports
(addresses in its hardware I/O address space), interrupt lines, and
DMA channels. When any device driver wants to access such a
resource, it is expected to reserve the resource with the kernel
database first. This requirement incidentally allows the system
administrator to determine exactly which resources have been
allocated by which driver at any given point.