UNIX PROGRAMMING
V Semester CSE/ISE
Module 4
Dr. Mallanagouda Patil
Associate Professor
RVITM, Bengaluru
Changing User IDs and Group IDs
When our programs need additional privileges, they need to
change their user or group ID to an ID that has the appropriate
privilege or access.
Similarly, when our programs need to lower their privileges or
prevent access to certain resources, they do so by changing either
their user IDs.
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
Changing User IDs and Group IDs
For a process, Real the user ID which
UserId is simply the determines what is basically the original effective
UserID of the user that currently allowed to user ID to be able to return to
has started it. It defines the original effective user ID
do and not allowed
which files that this
to do when necessary
process has access to.
• If the process has superuser privileges, the setuid function sets the
real user ID, effective user ID, and saved set-user-ID to uid.
• If the process does not have superuser privileges, but uid equals
either the real user ID or the saved set-user- ID, setuid sets only the
effective user ID to uid.
• If neither of these two conditions is true, errno is set to EPERM, and -
1 is returned.
Changing User IDs and Group IDs
• Only a superuser process can change the real user ID. Normally, the
real user ID is set by the login program when we log in and never
changes.
•
• The effective user ID is set by the exec functions only if the set-user-
ID bit is set for the program file.
• If the set-user-ID bit is not set, the exec functions leave the effective
user ID as its current value.
•
• The saved set-user-ID is copied from the effective user ID by exec.
• If the file's set-user-ID bit is set, this copy is saved after exec stores
the effective user ID from the file's user ID.
Setting the various user IDs
Interpreter Files
• An interpreter compiles the programs line by line rather than as a
whole.
• Shell scripts, perl scripts, awk scripts, and so forth are all examples
of interpreter files.
• In some operating systems, the command interpreter is called the
shell.
• These files are text files that begin with a line of the form
#! pathname [ optional-argument ]
• The space between the exclamation point and the pathname is
optional.
• The most common of these interpreter files begin with the line
Interpreter Files
• The recognition of these interpreter files is done within the kernel
as part of processing the exec system call.
• The actual file that gets executed by the kernel is not the interpreter
file, but the file specified by the pathname on the first line of the
interpreter file.
• Be sure to differentiate between the interpreter file and a text file
that begins with #! and the interpreter, which is specified by the
pathname on the first line of the interpreter file.
• Although the first line in the interpreter file is a comment but is not
treated so.
Interpreter Files
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid == 0)
{
if (execl("/home/sar/bin/testinterp", "testinterp", "myarg1", "MY
ARG2", (char *)0) < 0)
err_sys("execl error");
}
if (waitpid(pid, NULL, 0) < 0) /* parent */ err_sys("waitpid
error");
exit(0);
}
Interpreter Files
Output:
$ cat /home/sar/bin/testinterp #!/home/sar/bin/echoarg foo
$ ./a.out
argv[0]: /home/sar/bin/echoarg argv[1]: foo
argv[2]: /home/sar/bin/testinterp argv[3]: myarg1
argv[4]: MY ARG2
system function
• The system library function uses fork to create a child process that
executes the shell command specified in command using execl
function as follows:
execl("/bin/sh", "sh", "-c", command, (char *) NULL);
• The system function returns after the command has been
completed. If command is NULL, then system returns a status
indicating whether a shell is available on the system.
#include <stdlib.h>
int system(const char *cmdstring);
system function
Because system is implemented by calling fork, exec, and waitpid,
there are three types of return values.
If either the fork fails or waitpid returns an error other than EINTR,
system returns 1 with errno set to indicate the error.
If the exec fails, implying that the shell can't be executed, the return
value is as if the shell had executed exit(127).
Otherwise, all three functions fork, exec, and waitpid succeed, and
the return value from system is the termination status of the shell,
in the format specified for waitpid.
#include <sys/wait.h>
int main(void)
{
int status;
if ((status = system("date")) < 0) err_sys("system() error");
pr_exit(status);
if ((status = system("nosuchcommand")) < 0)
err_sys("system() error");
pr_exit(status);
if ((status = system("who; exit 44")) < 0)
err_sys("system() error");
pr_exit(status);
exit(0);
}
Command line arguments can also be executed using the system
function as shown in the following program.
int main(int argc, char *argv[])
{
int status;
if (argc < 2)
err_quit("command-line argument required");
if ((status = system(argv[1])) < 0)
err_sys("system() error");
pr_exit(status);
exit(0);
}
Process Accounting
• When enabled, the kernel writes an accounting record each time a
process terminates.
• Accounting records are typically a small amount of binary data.
• Accounting records contain the name of the command, the amount
of CPU time used, the user ID and group ID, the starting time, and so
on.
• A superuser executes accton function to enables and disable
process accounting.
Process Accounting
• CPU times and number of characters transferred, are kept by the
kernel in the process table and initialized whenever a new process
is created
• Each accounting record is written when the process terminates.
This has two consequences:
There are no accounting records for processes that never
terminate. For instance, the init process.
The order of the records in the accounting file corresponds to
the termination order of the processes not the order in which
they were started.
Process Accounting Structure
struct acct {
char ac_flag; /* flag */
char ac_stat; /* termination status (signal & core flag only) */
/* (Solaris only) */
uid_t ac_uid; /* real user ID */
gid_t ac_gid; /* real group ID */
dev_t ac_tty; /* controlling terminal */
time_t ac_btime; /* starting calendar time */
comp_t ac_utime; /* user CPU time (clock ticks) */
comp_t ac_stime; /* system CPU time (clock ticks) */
comp_t ac_etime; /* elapsed time (clock ticks) */
comp_t ac_mem; /* average memory usage */
comp_t ac_io; /* bytes transferred (by read and write) */
/* "blocks" on BSD systems */
comp_t ac_rw; /* blocks read or written */
/* (not present on BSD systems) */
char ac_comm[8]; /* command name: [8] for Solaris, */
/* [10] for Mac OS X, [16] for FreeBSD, and */
/* [17] for Linux */
};
The various values of ac_flag and their details
ac_flag Description
AFORK process is the result of fork, but never called exec
ASU process used superuser privileges
ACOMPAT process used compatibility mode
ACORE process dumped core
AXSIG process was killed by a signal
AEXPND expanded accounting
entry
Program to generate accounting data
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid != 0)
{ /* parent */
sleep(2);
exit(2); /* terminate with exit status 2 */
}
if ((pid = fork()) < 0) /* first child */
err_sys("fork error");
else if (pid != 0)
{
sleep(4);
abort(); /* terminate with core dump */
}
Program to generate accounting data
/* second child */
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid != 0)
{
execl("/bin/dd", "dd", "if=/etc/termcap", "of=/dev/null", NULL);
exit(7);
/* shouldn't get here */
}
/* third child */
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid != 0)
{
sleep(8);
exit(0); /* normal exit */
}
Program to generate accounting data
/* fourth child */
sleep(6);
kill(getpid(), SIGKILL); /* terminate w/signal, no core dump */
exit(6);
/* shouldn't get here */
}
Process Accounting
Each child does something different and then terminates.
The accounting records correspond to processes, not programs.
A new record is initialized by the kernel for the child after a fork,
not when a new program is executed.
This means that if we have a chain of three program A,B and C , the
program A execs B, B execs C and C exits.
Only a single accounting record is written.
The command name in the record corresponds to the program C
but the CPU times are the sum for programs A, B and C.
User Identification
• Sometimes, we want to find out the login name of the user who's
running the program.
• We can call getpwuid(getuid()), but what if a single user has
multiple login names, each with the same user ID?
• The system normally keeps track of the name we log in and the
getlogin function provides a way to fetch that login name
#include <unistd.h>
char *getlogin(void);
• This function can fail if the process is not attached to a terminal
that a user logged into. We normally call these processes as
daemons
Process Times
• There are three times that can be measured: wall clock time, user
CPU time, and system CPU time.
• The clock time (wall clock time) is the amount of time the process
takes to run and depends on the number of other processes being
run on the system.
• The user CPU time is the CPU time attributed to user instructions.
• The system CPU time is the CPU time attributed to the kernel when
it executes on behalf of the user process.
- For instance, whenever a process executes a system call, the
time
spent within the kernel performing that service is charged to
the
process.
• The sum of user CPU time and system CPU time is often called the
Process Times
• Any process can call the times function to obtain these values for
itself and any terminated children.
#include <sys/times.h>
clock_t times(struct tms *buf);
• Returns: elapsed wall clock time in clock ticks if OK otherwise -1 on
error.
• This function fills in the tms structure pointed to by buf:
struct tms {
clock_t tms_utime; /* user CPU time */
clock_t tms_stime; /* system CPU time */
clock_t tms_cutime; /* user CPU time, terminated children */
clock_t tms_cstime; /* system CPU time, terminated children
*/
};
Process Times
• Note that the structure does not contain any measurement for the
wall clock time.
• Instead, the function returns the wall clock time as the value of the
function, each time it's called.
• This value is measured from some arbitrary point in the past, so we
can't use its absolute value; instead, we use its relative value.
• For example, we call times and save the return value. At some later
time, we call times again and subtract the earlier return value from
the new return value.
• The difference is the wall clock time.
IO Redirection
• Redirecting standard input and output to somewhere else.
int main(int argc, char **argv)
{
int pid, status;
int newfd; /* new file descriptor */
if (argc != 2) {
fprintf(stderr, "usage: %s output_file\n", argv[0]);
exit(1);
}
if ((newfd = open(argv[1], O_CREAT|O_TRUNC|O_WRONLY, 0644)) < 0)
{
perror(argv[1]); /* open failed */
exit(1);
}
printf("This goes to the standard output.\n");
printf("Now the standard output will go to \"%s\".\n", argv[1]);
/* this new file will become the standard output */
/* standard output is file descriptor 1, so we use dup2 to */
/* to copy the new file descriptor onto file descriptor 1 */
/* dup2 will close the current standard output */
dup2(newfd, 1);
printf("This goes to the standard output too.\n");
exit(0);
}
Interprocess Communication
• Inter process communication (IPC) is used for exchanging data
between multiple processes.
• The processes may be running on single or multiple computers
connected by a network.
• Set of programming interfaces that allow a programmer to coordinate
activities among the processes which can run concurrently in an
operating system.
• This allows a specific program to handle many user requests at the
same time.
• The various forms of IPC that are supported on a UNIX system (same
host) are as follows: Pipes, FIFO’s , Message Queues and Semaphores.
Pipes
• Pipes are the oldest form of UNIX System IPC and are half duplex.
#include <unistd.h>
int pipe(int filedes[2]);
• Two file descriptors are returned through the filedes argument.
• The filedes[0] is open for reading, and filedes[1] is open for writing.
• The output of filedes[1] is the input for filedes[0]. Two ways to picture
a half-duplex pipe are shown in figure 4.3.
Pipes
• Pipes are the oldest form of UNIX System IPC and are half duplex.
#include <unistd.h>
int pipe(int filedes[2]);
Returns: 0 if OK, -1 on error.
• Two file descriptors are returned through the filedes argument.
• The filedes[0] is open for reading, and filedes[1] is open for writing.
• The output of filedes[1] is the input for filedes[0].
Pipes
Two ways to picture a half-duplex pipe.
Pipes
Normally, the process that calls pipe calls fork, creating an IPC channel
from the parent to the child or vice versa as shown .
Pipe from parent to child process
The parent writes to fd[1] and the child reads from fd[0].
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0) {
err_sys("fork error");
}
else if (pid > 0)
{ /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
}
else {
/* child */ close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
The above program creates a pipe between the parent and its
child and sends data down the pipe.
popen and pclose functions
• These two functions handle creating a pipe, forking a child, closing the
unused ends of the pipe, executing a shell to run the command, and
waiting for the command to terminate.
• Following are the general forms of popen and pclose functions.
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
• Returns: file pointer if OK, NULL on error
int pclose(FILE *fp);
• Returns: termination status of cmdstring, or -1 on error
popen and pclose functions
• The function popen does a fork and exec to execute the cmdstring, and
returns a standard I/O file pointer.
• If type is "r", the file pointer is connected to the standard output of
cmdstring.
• If type is "w", the file pointer is connected to the standard input of
cmdstring.
Co-processes
• A UNIX system filter is a program that reads from standard input and
writes to the standard output.
• Filters are normally connected linearly in shell pipelines.
• A filter becomes a co-process when the same program generates the
filter's input and reads the filter's output.
• A co-process normally runs in the background from a shell, and its
standard input and standard output are connected to another program
using a pipe.
Co-processes
The process creates two pipes: one is the standard input of the coprocess,
and the other is the standard output of the coprocess.
The following program is a simple coprocess that reads standard input and
writes to standard output, computes their sum, and writes the sum to its
standard output.
Co-processes
int main(void)
{
int n, int1, int2;
char line[MAXLINE];
while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0)
{
line[n] = 0; /* null terminate */
if (sscanf(line, "%d%d", &int1, &int2) == 2)
{
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
Co-processes
if (write(STDOUT_FILENO, line, n) != n)
err_sys("write error");
}
else
{
if(write(STDOUT_FILENO,"invalid args\n", 13) != 13)
err_sys("write error");
}
}
exit(0);
}
FIFOs
• Called as named pipes, FIFOs however, can be used with unrelated
processes.
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
• Returns: 0 if OK, -1 on error.
• Once we have used mkfifo to create a FIFO, we open it using open.
• When we open a FIFO, the nonblocking flag (O_NONBLOCK) affects
what happens next.
FIFOs
• In the normal case (O_NONBLOCK not specified), an open for read-only
blocks until some other process opens the FIFO for writing.
• Similarly, an open for write-only blocks until some other process opens
the FIFO for reading.
• If O_NONBLOCK is specified, an open for read-only returns immediately.
• But an open for write-only returns 1 with errno set to ENXIO if no
process has the FIFO open for reading.
• As with a pipe, if we write to a FIFO that no process has opened it for
reading, the signal SIGPIPE is generated.
FIFOs
• It is common to have multiple readers for a FIFO leading to the worry of
atomic operations from multiple processes to be synchronized.
• As with pipe, the constant PIPE_BUF specifies the maximum amount of
data that can be written atomically to a FIFO.
• There are two uses for FIFO:
FIFOs are used by shell commands to pass data from one shell
pipeline to another without creating intermediate temporary files.
FIFOs are used as rendezvous points in client-server applications to
pass data between the clients and the servers.
Using FIFOs to duplicate output streams
• It is common to FIFOs can be used to duplicate an output stream in a
series of shell commands.
• This prevents writing the data to an intermediate disk file.
• With a FIFO and the UNIX program tee(1), we can accomplish this
procedure without using a temporary file.
• The tee program copies its standard input to both its standard output
and to the file named on its command line.
mkfifo fifo1 prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2
Using FIFOs to duplicate output streams
• Create the FIFO and then start prog3 in the background, reading from
the FIFO.
• Then start prog1 and use tee to send its input to both the FIFO and
prog2.
Client Server Communication using FIFO
Clients sending requests to a server using a FIFO.
Client-Server Communication using FIFO
Clients sending requests to a server using a FIFO.
Problem with this arrangement is that how to send reply from the server to
each client.
Clients-Server Communication using FIFO
One solution is that for each client to send its process id with the request.
Server then creates a unique FIFO for each client using pathname based on
the client ID.
Message Queues
• Linked list of messages stored within the kernel and identified by a
message queue identifier.
• A new queue is created or an existing queue opened by msgget.
• New messages are added to the end of a queue by msgsnd.
• Every message has a positive long integer type field, a non-negative
length, and the actual data bytes (corresponding to the length), all of
which are specified to msgsnd.
• Messages are fetched from a queue by msgrcv. Don't have to fetch the
messages in a first-in, first-out order. Instead, we can fetch messages
based on their type field.
Message Queues
struct msqid_ds {
struct ipc_perm msg_perm;
msgqnum_t msg_qnum;
msglen_t msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
time_t msg_stime; /* last-msgsnd() time */
time_t msg_rtime; /* last-msgrcv() time */
time_t msg_ctime; /* last-change time */
.
.
};
Message Queues
• msgget function opens an existing queue or creates a new one.
#include <sys/msg.h>
int msgget(key_t key, int flag);
• When a new queue is created, the following members of the msqid_ds
structure are initialized.
The ipc_perm structure is initialized.
The mode member of this structure is set to the corresponding
permission bits of flag.
The msg_qnum, msg_lspid, msg_lrpid, msg_stime, and
msg_rtime are
all set to 0.
The msg_ctime is set to the current time.
The msg_qbytes is set to the system limit.
Semaphores
A counter used to provide access to a shared data object for multiple
processes. To obtain a shared resource, a process needs to do the following:
1. Test the semaphore that controls the resource.
2. If the value of the semaphore is positive, the process can use the
resource. In this case, the process decrements the semaphore value by 1,
indicating that it has used one unit of the resource.
3. Otherwise, if the value of the semaphore is 0, the process goes to sleep
until the semaphore value is greater than 0. When the process wakes up, it
returns to step 1.