330 likes | 472 Views
Tutorial: CSI 3310. Dewan Tanvir Ahmed SITE, UofO. Today’s Objective. Go through the last lab Play with exec() fork() Dup2 pipe. Process. A process in Unix terms is an instance of an executing program.
E N D
Tutorial: CSI 3310 Dewan Tanvir Ahmed SITE, UofO
Today’s Objective • Go through the last lab • Play with • exec() • fork() • Dup2 • pipe
Process • A process in Unix terms is an instance of an executing program. • Each process incorporates program code, the data values within program variables, and values held in hardware registers, program stack etc. • Associated with each process is a process control block (PCB) which stores all the information related to the process. • fork .... Creates a new process. • exec .... Overlays the memory space of the process with a new program. • wait .... synchronizes processes. • exit .... Terminates a process
fork() • fork() creates a new child process • Points to note • the OS copies the current program into the new process, • resets the program pointer to the start of the new program (child fork location), and • both processes continue execution independently as two separate processes • The child gets its own copy of the parent’s: • data segments • heap segment • stack segment • file descriptors
fork() Return Values • fork() is the one Unix function that is called once but returns twice!!: • If fork() returns 0: • you’re in the new child process • If fork() returns > 1 (i.e., the pid of the new child process) • you’re back in the parent process
Waiting on Our Children • Unlike life, parents should always hang around for their children’s lives (runtimes) to end, that is to say: • Parent processes should always wait for their child processes to end • When a child process dies, a SIGCHLD signal is sent to the parent as notification • The SIGCHLD signal’s default disposition is to ignore the signal • A parent can find out the exit status of a child process by calling one of the wait() functions
Problem Children:Orphans and Zombies • If a child process exits before it’s parent has called wait(), it would be inefficient to keep the entire child process around, since all the parent is going to want to know about is the exit status: • A zombie is a child process that has exited before it’s parent’s has called wait() for the child’s exit status • A zombie holds nothing but the child’s exit status (held in the program control block) • Modern Unix systems have init (pid == 1) adopt zombies after their parents die, so that zombies do not hang around forever as they used to, in case the parent never did get around to calling wait
Points to note #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main(void){ pid_t fork_pid; FILE *my_file; my_file=fopen("MY_FORK_FILE.txt","w"); fprintf(my_file," Hello from main \n"); fork_pid=fork(); fprintf(my_file," After fork %d\n",fork_pid); return 0; } Hello from main After fork 0 Hello from main After fork 14818 The first thing you should note is that the output from the child process appears in the same file as the parent process. One characteristic of fork is that all file descriptors that are open in the parent are duplicated in the child.
Exec • The only way in which • a program is executed in Unix is for an existing program to issue an exec system call. • The exec calls overlays the memory space of the calling process with a new program. • When used together, fork and exec provides the means for the programmer to start another program, yielding multitasking.
The exec() Functions:Out with the old, in with the new • The exec() functions all replace the current program running within the process with another program • There are two families of exec() functions, • the “l” family (list), and • the “v” family (vector) • What is the return value of an exec() call?
The execl... functions • int execl(const char * path, const char * arg0, ...); • executes the command at path, • passing it the environment as a list: arg0 ... argn • thus, the execl family breaks down argv into its individual constituents, and then passes them as a list to the execl? • int execlp(const char * path, const char * arg0, ...); • same as execl, but • uses $PATH resolution for locating the program in path, thus an absolute pathname is not necessary • int execle(const char * path, const char * arg0, ... char * envp[]); • allows you to specifically set the new program’s environment, which replaces the default current program’s environment
The execv... functions • int execv(const char * path, char *const argv[]); • executes the command at path, • passing it the environment contained in a single argv[] vector • int execvp(const char * path, char *const argv[]); • same as execv, • but uses $PATH resolution for locating the program in path • int execve(const char * path, char *const argv[], char * const envp[]); • note that this is the only system call of the lot
File Descriptor • Contains • reference to system file structure • Denoted by small integer • passed to system calls to specify which process-to-file connection should be affected • Created by open( ), dup( ), dup2( ), pipe( ), socket( ) • Removed by close( ) • Duplicated by fork() • Retained across exec() (unless FD_CLOEXEC)
Standard File Descriptors • Each process normally has file descriptors for standard input, standard output, standard error • Denoted by • STDIN_FILENO • STDOUT_FILENO • STDERR_FILENO • Older programs use 0, 1, 2 • Provide unbuffered access to data in files via • read( ), write( ), lseek( ) • Buffered FILE structures stdin (scanf), stdout (printf), stderr are layered on unbuffered files
Inter Process Communication (IPC) • For processes to cooperate in performing a task, they need to share data. • The fundamentals of network programming lies in processes' ability to share data among themselves. • Unix allows concurrent processes to communicate by using a variety of IPC methods • Signals • message queues • shared memory • semaphores and • sockets.
Pipe • Pipe is called with a two integer array • hold the file descriptors associated with the pipe • the first element in the array (p[0]) opens the pipe for reading • while the other file descriptor (p[1]) opens the pipe for writing. • Once created we can use pipes to communicate with the process itself or with any child process.
Concept… Stage 1 The original process A (the shell for example) creates a pipe, receiving descriptors on each end. Stage 2 Process A forks twice, creating child processes B and C. The children inherit the parent's open descriptors, including those on the two ends of the pipe, so at this stage, all three processes have descriptors on both ends of the pipe.
Concept… Stage 3 Each process closes the descriptors it doesn't need. B closes the downstream end, C closes the upstream end, and A closes both. Note that B and C inherit their own copies of A's descriptors, so that closing a descriptor in the parent doesn't affect the child's descriptors.
Concept… Stage 4 Processes B and C exec() the programs they need to run. Again, the descriptors remain intact across the exec() call. B can then write to the upstream end of the pipe, and C can read from the downstream end. Inter Process communication!!!
Pipe • Note: A buffer is a system maintained data structure accessible to both ends of the pipe. • The writer writes on to the buffer while the reader reads from it. • How do we access the pipe? • Obviously the array is not going to exist after the exec(), so how do we access the pipe? • Before the exec(), we have to reassign the standard output of the process to the pipe. • We can do this using a system call to duplicate file descriptors called “dup2()”
dup2() • dup2(target, new) • The file descriptor new will point to the same file as target after the call. • If our target is the standard output stream of the process, then we will redirect the standard output to the pipe. • The standard output will survive the exec() call.
close() • The close() system call is used to close pipes and files. • We need to close() the open pipes in the child process before we exec() the new process. • If not, the open pipes may be destroyed by the exec() call, also destroying our newly created connection.
Example: ls > temp Create a subprocess from program “ls”; redirect standard output of “ls” into file named “temp” Unix shell notation: $ ls > temp int main(int argc, char *argv[]) { pid = fork(); if (pid == 0) { fd = open("temp", O_WRONLY|O_CREAT|O_TRUNC, S_IRWXU); dup2(fd, STDOUT_FILENO); if (execl(“/bin/ls”, “ls”, NULL) == -1) perror("execl"); } else { close(fd); wait(&status); }}
Example – Execl/Execlp #include <unistd.h> #include <stdio.h> int main(void){ int status; if (fork()){ // Parent: Do you know why? } else { // Child printf(" HELLO from Child \n"); status=execl("/bin/ls","ls","-l","-t",'\0'); if (status != 0)perror("execl error:"); } return 0; }
Output HELLO from Child {ncitl0}dahmed(61)$ total 664 -rwx------ 1 dahmed grad 5082 May 15 17:37 a.out -rw-r--r-- 1 dahmed grad 352 May 15 17:37 exels.c -rw-r--r-- 1 dahmed grad 528 May 15 17:06 pipe.c -rwx------ 1 dahmed grad 6371 May 15 17:01 mymon -rw-r--r-- 1 dahmed grad 1938 May 15 17:01 mymon.c -rw------- 1 dahmed grad 289 May 15 16:08 sat.c -rw-r--r-- 1 dahmed grad 478 May 15 16:07 redir.c -rw------- 1 dahmed grad 75 May 15 15:28 exeSleep.c -rwx------ 1 dahmed grad 4831 May 15 15:12 sat -rwx------ 1 dahmed grad 5779 May 15 14:00 pt -rw-r--r-- 1 dahmed grad 561 May 15 14:00 pt.c -rwx------ 1 dahmed grad 6272 May 15 12:27 procmon -rw-r--r-- 1 dahmed grad 2379 May 15 05:15 procmon.c drwx------ 3 dahmed grad 512 May 15 04:50 lab1 -rw------- 1 dahmed grad 521096 May 14 11:12 Trash -rw-r--r-- 1 dahmed grad 61440 May 9 14:17 lab1.tar drwxr-xr-x 18 dahmed grad 1024 May 8 16:43 public_html drwx------ 2 dahmed grad 512 Apr 29 21:44 Mail drwxr-xr-x 2 dahmed grad 512 Aug 23 2005 pine drwxr-xr-x 6 dahmed grad 512 Aug 23 2005 ssh drwxr-xr-x 7 dahmed grad 512 Aug 23 2005 netscape -rw-r--r-- 1 dahmed grad 11576 Jan 21 2004 calcloop
Question? • Modify the above code so that it works when "/bin/ls" is replaced with "ls", i.e. which version of exec should you now be using? status=execlp("ls","ls","-l","-t",'\0');
Simple pipe Example… int main() { int i,j,p[2]; char s[100]; char s2[100]; i=pipe(p); printf("pipe returned %d %d %d\n",i,p[0],p[1]); for (j=0; j<5; j++){ sprintf(s,"I am a string %d\n",j); i=write(p[1],s,strlen(s)); printf("Write completed i=%d\n",i); i=read(p[0],s2,100); printf("Read from the pipe completed i=%d\n\t%s\n",i,s2); } }
Sample output pipe returned 0 3 4 Write completed i=16 Read from the pipe completed i=16 I am a string 0 Write completed i=16 Read from the pipe completed i=16 I am a string 1 Write completed i=16 Read from the pipe completed i=16 I am a string 2 Write completed i=16 Read from the pipe completed i=16 I am a string 3 Write completed i=16 Read from the pipe completed i=16 I am a string 4
Example: dup2 and pipe… case 0: close(p[0]); dup2(p[1], 1); close(p[1]); execlp("ls", "ls", (char *)0); break; default: read(p[0], inbuf, MSGSIZE); printf("%s", inbuf); close(p[0]); close(p[1]); } } #include <stdio.h> #include <unistd.h> #define MSGSIZE 4096 main() { char inbuf[MSGSIZE]; int p[2], j; pid_t pid; if (pipe(p) == -1) { printf("Error opening pipe\n"); exit(1); } switch(pid = fork()) { case -1: perror("Error with the fork()\n"); exit(2);
Pipe drawbacks • Pipes can only be used to connect processes that have a common ancestry, eliminating the possibility for a true client-server application to exist. • Pipes cannot be permanent, as they have to be created and destroyed with the process.