Tag Archives: unix
C++ || File Copier Using Memory Mapped Files
The following is another homework assignment which was presented in an Operating Systems Concepts class. Using commandline arguments, the following is a program which implements a file copier using memory mapped files. This program makes use of the “mmap” function call provided on Unix based systems.
REQUIRED KNOWLEDGE FOR THIS PROGRAM
How To Use Memory Mapped Files
How To Get The Size Of A File
How To Create A File Of Any Size
==== 1. OVERVIEW ====
A memory mapped file is a segment of virtual memory which has been assigned a direct byte for byte correlation with some portion of a file or file like resource. The primary benefit of memory mapping a file is increasing I/O performance, especially when used on large files. Accessing memory mapped files is faster than using direct read and write operations. Memory mapped files are designed to simplify and optimize file access.
The program demonstrated on this page works with any file type (i.e: txt, cpp, jpg, png, mp4, flv, etc.) and copies the contents of one file, and saves it into another separate output file.
==== 2. TECHNICAL DETAILS ====
This program has the following flow of control:
1. The program is invoked with the source file name and the destination file name as commandline arguments.
2. The program uses the mmap() system call to map both files into memory.
3. The program uses the memory-mapped file memory to to copy the source file to the destination file.
This program is also modified in such a way that you cannot do an mmap on the same file. For example, if you have an input file “A” as the source file, the destination file “B” needs to have a different filename. If both the source and the destination files have the same filename, an error message is displayed, and the program exits. In order for this program to work, the source and the destination files must have different names.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
// ============================================================================ // Author: K Perkins // Date: Oct 5, 2013 // Taken From: http://programmingnotes.org/ // File: Mcp.cpp // Description: This program implements a file copier based on memory // mapped files. // This program has the following flow of control: // (1) The program is invoked with the source file name and the // destination file name as parameters. // (2) The program uses the mmap() system call to map both files into // memory. // (3) The program uses the memory-mapped file memory to copy the // source file to the destination file. // ============================================================================ #include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/mman.h> using namespace std; int main(int argc, char* argv[]) { // declare variables struct stat statInfo; int srcFd = -1; int destFd = -1; int pagesize = -1; int mappingSize = -1; int leftToWrite = -1; off_t offset = 0; char* srcBuff = NULL; char* destBuff = NULL; const char* srcFileName = NULL; const char* destFileName = NULL; FILE* destFp = NULL; // check if theres enough commandline args if(argc < 3) { cerr<<"n** ERROR NOT ENOUGH ARGS!n" <<"nUSAGE: "<<argv[0]<<" <SOURCE FILE NAME> <DESTINATION FILE NAME>nn"; exit(1); } // set the pointers to the source and destination files srcFileName = argv[1]; destFileName = argv[2]; // check if the source and the destination file have the same name if(strncmp(srcFileName, destFileName, sizeof(srcFileName)) == 0) { cerr<<"n** ERROR - The source and destination files both have the " <<"same name..nnPlease select a name other than "" <<srcFileName<<"" for the destination file!nn"; exit(1); } // get the file information if(stat(srcFileName, &statInfo) < 0) { perror("stat error"); exit(1); } // open the destination file destFp = fopen(destFileName, "w+"); // make sure it opened ok if(!destFp) { perror("open error"); exit(1); } // display info to the screen cerr<<"nThe source file ""<<srcFileName<<"" is " <<statInfo.st_size<<" bytesn"; // set the pointer to the end of the file if(fseek(destFp, statInfo.st_size - 1, SEEK_SET) < 0) { perror("fseek error"); exit(1); } // write a single byte to make the destination file // the same size as the "source" file if(fwrite("", 1, sizeof(""), destFp) < 0) { perror("write error"); exit(1); } // close the destination file fclose(destFp); // open the source file for reading srcFd = open(srcFileName, O_RDONLY); // make sure the file was opened successfully if(srcFd < 0) { perror("open error"); exit(1); } // open the destination file again, this time for actual writing destFd = open(destFileName, O_RDWR); // make sure the destination file was opened successfully if(destFd < 0) { perror("open error"); exit(1); } // get the size of the page pagesize = getpagesize(); // how many bytes left to write leftToWrite = statInfo.st_size; // copy the bytes of the source file to the destination file while(leftToWrite) { // if there is more than pagesize bytes, // then map pagesize bytes. Otherwise, // map the remaining bytes if(leftToWrite > pagesize) { mappingSize = pagesize; } else { mappingSize = leftToWrite; } //cerr<<"mappingSize = "<<mappingSize<<endl; // map the pagesized source file if((srcBuff = (char*)mmap(NULL, mappingSize, PROT_READ, MAP_SHARED, srcFd, offset)) == (void*)-1) { perror("mmap1 error"); exit(1); } // map the pagesized destination file if((destBuff = (char*)mmap(NULL, mappingSize, PROT_READ|PROT_WRITE, MAP_SHARED, destFd, offset)) == (void*)-1) { perror("mmap2 error"); exit(1); } // copy the source to destination memcpy(destBuff, srcBuff, mappingSize); // update the number of bytes left to write leftToWrite -= mappingSize; // update the offset offset += mappingSize; // unmap both buffers if(munmap((void*)srcBuff, mappingSize) < 0) { perror("munmap1 error"); exit(1); } if(munmap((void*)destBuff, mappingSize) < 0) { perror("munmap2 error"); exit(1); } } // display info to the screen cerr<<"nThe destination file "" <<destFileName<<"" has been created and is "<<offset<<" bytesnn"; // close both file descriptors close(srcFd); close(destFd); return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
./mcp 1.png 2.png
The source file "1.png" is 42004 bytes
The destination file "2.png" has been created and is 42004 bytes
C++ || Snippet – How To Create A File Of Any Size
The following is sample code which demonstrates how to create a file of any size using the fseek and fwrite function calls.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
// ============================================================================ // Author: K Perkins // Date: Oct 4, 2013 // Taken From: http://programmingnotes.org/ // File: createfileN.cpp // Description: Demonstrate how to create a file of any size // ============================================================================ #include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> using namespace std; int main(int argc, char* argv[]) { // declare variables int fileSize = 0; FILE* fp = NULL; // check the command line arguments if(argc < 3) { cerr<<"n** ERROR NOT ENOUGH ARGS!n" <<"nUSAGE: "<<argv[0]<<" <FILE NAME> <FILE SIZE>nn"; exit(1); } // convert the file size from string to integer fileSize = atoi(argv[2]); // open the file to be created fp = fopen(argv[1], "w+"); // make sure the file was created ok if(!fp) { perror("open error"); exit(1); } // set the file marker to position N in the file (where N = file size) if(fseek(fp, fileSize - 1, SEEK_SET) < 0) { perror("fseek error"); exit(1); } // write a single byte to the file if(fwrite(" ", sizeof(char), strlen(" "), fp) < 0) { perror("write error"); exit(1); } // close the file fclose(fp); return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
./createfileN createfile.txt 12
[FILE "createfile.txt" IS CREATED AND IS 12 BYTES IN SIZE]
C++ || Snippet – How To Display The Size Of A File Using Stat
The following is sample code which demonstrates the use of the “stat” function calls on Unix based systems.
The stat function returns information about a file. No permissions are required on the file itself, but-in the case of stat() and lstat() – execute (search) permission is required on all of the directories in path that lead to the file.
The “stat” system call returns a stat structure, which contains the following fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* inode number */ mode_t st_mode; /* protection */ nlink_t st_nlink; /* number of hard links */ uid_t st_uid; /* user ID of owner */ gid_t st_gid; /* group ID of owner */ dev_t st_rdev; /* device ID (if special file) */ off_t st_size; /* total size, in bytes */ blksize_t st_blksize; /* blocksize for file system I/O */ blkcnt_t st_blocks; /* number of 512B blocks allocated */ time_t st_atime; /* time of last access */ time_t st_mtime; /* time of last modification */ time_t st_ctime; /* time of last status change */ }; |
The following example demonstrates the use of the “st_size” struct parameter to display the size of a file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// ============================================================================ // Author: K Perkins // Date: Oct 4, 2013 // Taken From: http://programmingnotes.org/ // File: Stat.cpp // Description: Demonstrate the use of the "stat" function to display the // size of a file // ============================================================================ #include <iostream> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> using namespace std; int main(int argc, char* argv[]) { // declare variables struct stat fileInfo; // the buffer to store file information // check to see if theres enough commandline args if(argc < 2) { cerr<<"n** ERROR - NOT ENOUGH ARGUMENTS!n" <<"nUSAGE: "<<argv[0]<<" <FILE NAME>n"; exit(1); } // get the file information if(stat(argv[1], &fileInfo) < 0) { cerr<<"nstat failed (file not found)n"; exit(1); } // display info to the screen cerr<<"nThe file ""<<argv[1]<<"" is "<<(int)fileInfo.st_size<<" bytesnn"; return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
./Stat Stat.cpp
The file "Stat.cpp" is 1170 bytes
C++ || Snippet – Multi-Process Synchronization Producer Consumer Problem Using Threads
The following is sample code which demonstrates the use of POSIX threads (pthreads), aswell as pthread mutex and condition variables for use on Unix based systems.
The use of mutual exclusion (mutex) variables refers to the requirement of ensuring that no two processes or threads are in their critical section at the same time. Here, a critical section refers to the period of time in which a process accesses a shared resource, such as shared memory. This procedure highlights a classic example of a multi-process synchronization problem known as the producer–consumer problem.
The producer–consumer problem describes a scenario in which two processes (the producer and the consumer) share a common resource (i.e: a string buffer). In this scenario, the producer’s job is to generate a piece of data, update that data with the shared resource (the buffer), and repeat. At the same time, the consumer is consuming that shared resource (i.e: removing something from that same buffer) one piece at a time. The problem is to make sure that the producer wont try to manipulate that shared resource (i.e: add data into the buffer) while the consumer is accessing it; and that the consumer wont try to remove data from that shared resource (the buffer) while the producer is updating it.
In this instance, a solution for the producer can be to go to sleep if a synchronization conflict occurs, and the next time the consumer removes an item from the buffer, it can notify the producer to wake up and start to fill the buffer again. In the same way, the consumer can go to sleep when the producer is accessing it. The next time the producer puts data into the buffer, it wakes up the sleeping consumer and the process continues.
The example on this page demonstrates the use of the above solution using the “pthread_mutex_lock()” function call to limit access to the critical section, and the “pthread_cond_wait()” function call to simulate the sleeping process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
// ============================================================================ // Author: Kenneth Perkins // Date: Oct 3, 2013 // Taken From: http://programmingnotes.org/ // File: Condvar.cpp // Description: Demonstrate the use of the producer consumer problem // using mutex and condition variables // ============================================================================ #include <iostream> #include <cstdlib> #include <pthread.h> using namespace std; // Compile & Run // g++ Condvar.cpp -lpthread -o Condvar // ./Condvar // function prototypes void* Produce(void* arg); void* Consume(void* arg); // global variables // the mutex and the condition variable pthread_mutex_t mutex; pthread_cond_t cond; // the condition flag bool condition = false; // the item produced int count = 0; // how much to produce const int NUM_TO_PRODUCE = 6; int main() { // declare variables pthread_t producerThread; pthread_t consumerThread; // initialize the mutex and the condition variable pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); cout <<"\nThe Parent is creating the producer and consumer threads..\n" << endl; // create a producer thread if(pthread_create(&producerThread, NULL, Produce, NULL) < 0) { perror("pthread_create"); exit(1); } // create a consumer thread if(pthread_create(&consumerThread, NULL, Consume, NULL) < 0) { perror("pthread_create"); exit(1); } // wait for the threads to complete // similar to the wait() system call for fork() processes if(pthread_join(producerThread, NULL) < 0) { perror("pthread_join"); exit(1); } if(pthread_join(consumerThread, NULL) < 0) { perror("pthread_join"); exit(1); } cout <<"\nBoth threads have completed and have terminated!\n" <<"\nThe Parent is now exiting...\n"; return 0; }// end of main /** * The producer thread function * @param arg - pointer to the thread local data - unused */ void* Produce(void* arg) { // produce things until the loop condition is met while(count < NUM_TO_PRODUCE) { // lock the mutex to protect the condition variable if(pthread_mutex_lock(&mutex) < 0) { perror("pthread_mutex_lock"); exit(1); } // we have produced something that has not been // consumed yet, so we sleep (wait) until the consumer // wakes us up. while(condition) { // sleep (wait) on a condition variable until the // the consumer wakes the producer up. if(pthread_cond_wait(&cond, &mutex) < 0) { perror("pthread_cond_wait"); exit(1); } } // produce an item cout <<"Produced: "<<++count<<endl; // we have produced something condition = true; // wake up the sleeping consumer if(pthread_cond_signal(&cond) < 0) { perror("pthread_cond_signal"); exit(1); } // release the mutex lock if(pthread_mutex_unlock(&mutex) < 0) { perror("pthread_mutex_unlock"); exit(1); } } return 0; }// end of Produce /** * The consumer function * @param arg - pointer to the thread local data - unused */ void* Consume(void* arg) { // consume things until the loop condition is met while(count < NUM_TO_PRODUCE) { // lock the mutex to protect the condition variable if(pthread_mutex_lock(&mutex) < 0) { perror("pthread_mutex_lock"); exit(1); } // if there is nothing to consume, then sleep while(!condition) { // sleep (wait) on a condition variable until the // producer wakes the consumer up. if(pthread_cond_wait(&cond, &mutex) < 0) { perror("pthread_cond_wait"); exit(1); } } cout <<"Consumed: "<<count<<endl<<endl; // consume the item condition = false; // wake up the sleeping producer if(pthread_cond_signal(&cond) < 0) { perror("pthread_cond_signal"); exit(1); } // unlock the mutex if(pthread_mutex_unlock(&mutex) < 0) { perror("pthread_mutex_unlock"); exit(1); } } return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
To use pthreads, the compiler flag “-lpthread” must be set.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
The Parent is creating the producer and consumer threads..
Produced: 1
Consumed: 1Produced: 2
Consumed: 2Produced: 3
Consumed: 3Produced: 4
Consumed: 4Produced: 5
Consumed: 5Produced: 6
Consumed: 6Both threads have completed and have terminated!
The Parent is now exiting...
C++ || Snippet – How To Create And Use Threads For Interprocess Communication
The following is sample code which demonstrates the use of POSIX threads (pthreads), aswell as the pthread_create, and pthread_join function calls on Unix based systems.
Much like the fork() function call which is used to create new processes, threads are similar in that they too are used for interprocess communication. Threads allow for multi-threading, which is a widespread programming and execution model that allows for multiple threads to exist within the same context of a single program.
Threads share the calling parent process’ resources. Each process has it’s own address space, but the threads within the same process share that address space. Threads also share any other resources within that process. This means that it’s very easy to share data amongst threads, but it’s also easy for the threads to step on each other, which can lead to bad things.
The example on this page demonstrates the use of multiple pthreads to display shared data (an integer variable) to the screen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
// ============================================================================ // Author: Kenneth Perkins // Date: Oct 3, 2013 // Taken From: http://programmingnotes.org/ // File: Pthread.cpp // Description: Demonstrate the use of using multiple threads using the // pthread_create() function // ============================================================================ #include <iostream> #include <cstdlib> #include <pthread.h> using namespace std; // Compile & Run // g++ Pthread.cpp -lpthread -o Pthread // ./Pthread <NUMBER OF THREADS TO CREATE> // function prototype void* ThreadFunc(void* args); // global variable int threadNum = 0; int main(int argc, char* argv[]) { // declare variables int numberOfThreads = 0; pthread_t* threads; // make sure theres enough commandline arguments // and EXIT if theres not if(argc < 2) { cout <<"** ERROR - NOT ENOUGH ARGS!\n" <<"\nUSAGE:"<<argv[0]<<" <NUMBER OF THREADS TO CREATE>\n"; exit(1); } // convert the number of threads from the command line // from string to int value numberOfThreads = atoi(argv[1]); // declare the array of thread variables threads = new pthread_t[numberOfThreads]; cout <<"\nThe Parent is creating "<<numberOfThreads<<" threads!" << endl; // use a loop to create the threads for(int x = 0; x < numberOfThreads; ++x) { // create the thread and call the "ThreadFunc" if(pthread_create(&threads[x], NULL, ThreadFunc, NULL) < 0) { perror("pthread_create error"); exit(1); } } cout <<"The Parent is now waiting for the thread(s) to complete...\n\n"; // wait for all threads to finish for(int x = 0; x < numberOfThreads; ++x) { pthread_join(threads[x], NULL); } cout <<"\nAll thread(s) are complete and have terminated!\n" <<"\nThe Parent is now exiting...\n"; return 0; }// end of main /** * The function called by the thread * @param args - pointer to the thread local data */ void* ThreadFunc(void* args) { // display the thread info cout <<"Hi, Im thread #"<<++threadNum <<" and this is my id number: "<<(unsigned)pthread_self()<<endl; return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
So, what is the difference between fork() processes and thread processes? Threads differ from traditional multitasking operating system processes in that:
• processes are typically independent, while threads exist as subsets of a process
• processes carry considerable state information, whereas multiple threads within a process share state as well as memory and other resources
• processes have separate address spaces, whereas threads share their address space
• processes interact only through system-provided inter-process communication mechanisms.
• Context switching between threads in the same process is typically faster than context switching between processes.
To use pthreads, the compiler flag “-lpthread” must be set.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
./Pthread 5
The Parent is creating 5 threads!
The Parent is now waiting for the thread(s) to complete...Hi, Im thread #1 and this is my id number: 1664468736
Hi, Im thread #2 and this is my id number: 1647683328
Hi, Im thread #3 and this is my id number: 1656076032
Hi, Im thread #4 and this is my id number: 1672861440
Hi, Im thread #5 and this is my id number: 1681254144All thread(s) are complete and have terminated!
The Parent is now exiting...
C++ || Snippet – How To Override The Default Signal Handler (CTRL-C)
The following is sample code which demonstrates the use of the “signal” function call on Unix based systems.
Signals are interrupts delivered to a process by the operating system which can terminate a program prematurely. You can generate interrupts by pressing Ctrl+C. The “signal” function call receives two arguments. The first argument is an integer which represents the signal number, and the second argument is a pointer to the user defined signal handling function.
The following program catches the “SIGINT” signal number using the signal() function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// ============================================================================ // Author: K Perkins // Date: Oct 3, 2013 // Taken From: http://programmingnotes.org/ // File: Signal.cpp // Description: Demonstrate the use of overriding the default signal // handler for the case when the user presses Ctrl-C. Test it by // running and pressing Ctrl-C // ============================================================================ #include <iostream> #include <csignal> #include <unistd.h> using namespace std; // function prototype void SignalHandlerFunc(int arg); // global variable int count = 10; int main() { // overide the default signal handler (CTRL-C) // with our own "SignalHandlerFunc" function signal(SIGINT, SignalHandlerFunc); cerr<<"nPlease press CTRL-Cnn"; // loop until condition is met do{ sleep(1); }while(count > 0); // press ENTER on the keyboard to end // the program once all "lives" are lost cin.get(); return 0; }// end of main /** * This function handles the signal * @param arg - the signal number */ void SignalHandlerFunc(int arg) { // display text when user presses CTRL-C if(count > 0) { cerr<<" Haha I have "<<count<<" lives!n"; } else { cerr<<" ** Ahh you got me...n"; } --count; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
Please press CTRL-C
^C Haha I have 10 lives!
^C Haha I have 9 lives!
^C Haha I have 8 lives!
^C Haha I have 7 lives!
^C Haha I have 6 lives!
^C Haha I have 5 lives!
^C Haha I have 4 lives!
^C Haha I have 3 lives!
^C Haha I have 2 lives!
^C Haha I have 1 lives!
^C ** Ahh you got me...
C++ || Multi-Hash Interprocess Communication Using Fork, Popen, & Pipes
The following is another homework assignment which was presented in an Operating Systems Concepts class. Using two pipes, the following is a program which implements the computing of hash values on a file using the MD5, SHA1, SHA224, SHA256, SHA384, and SHA512 hashing algorithms provided on Unix based systems.
REQUIRED KNOWLEDGE FOR THIS PROGRAM
==== 1. OVERVIEW ====
Hash algorithms map large data sets of variable length (e.g. files), to data sets of a fixed length. For example, the contents of a 1GB file may be hashed into a single 128-bit integer. Many hash algorithms exhibit an important property called an avalanche effect – slight changes in the input data trigger significant changes in the hash value.
Hash algorithms are often used for verifying the integrity of files downloaded from the WEB. For example, websites hosting a file usually post the hash value of the file using the MD5 hash algorithm. By doing this, the user can then verify the integrity of the downloaded file by computing the MD5 algorithm on their own, and compare their hash value against the hash value posted on the website. The user will know if the download was valid only if the two hash values match.
==== 2. TECHNICAL DETAILS ====
The following implements a program for computing the hash value of a file using the MD5, SHA1, SHA224, SHA256, SHA384, and SHA512 hashing algorithms provided on Unix based systems.
This program takes the name of the target file being analyzed as a command line argument, and does the following:
1. Check to make sure the file exists.
2. Create two pipes.
3. Create a child process.
4. The parent transmits the name of the file to the child (over the first pipe).
5. The child receives the name of the file and computes the hash of the file using the MD5 algorithm (using Linux program md5sum).
6. The child transmits the computed hash to the parent (over the second pipe) and terminates.
7. The parent receives the hash, prints it, and calls wait().
8. Repeat the same process starting with step 3, but using algorithms SHA1...SHA512.
9. The parent terminates after all hashes have been computed.
The use of the popen function is used in order to launch the above programs and capture their output into a character array buffer.
This program also uses two pipes. The two pipes created are the following:
(1) Parent to child pipe: Used by the parent to transfer the name of the file to the child. The parent writes to this pipe and the child reads it.
(2) Child to parent pipe: Used by the child to transfer the computed hashes to the parent. The child writes to this pipe and the parent reads it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
// ============================================================================= // Author: K Perkins // Date: Aug 20, 2013 // Taken From: http://programmingnotes.org/ // File: Multihash.cpp // Description: Hash algorithms map large data sets of variable length (e.g. // files), to data sets of a fixed length. For example, contents of a // 1GB file may be hashed into a single 128-bit integer. Using 2 pipes, // this program implements the computing of hash values on a file using // the MD5, SHA1, SHA224, SHA256, SHA384, and SHA512 hashing algorithms. // // The two pipes created are the following: // (1) Parent to child pipe: Used by the parent to transfer the name of // the file to the child. The parent writes to this pipe and the // child reads it. // (2) Child to parent pipe: Used by the child to transfer the computed // hashes to the parent. The child writes to this pipe and the // parent reads it. // ============================================================================= #include <iostream> #include <fstream> #include <cstdlib> #include <cstring> #include <unistd.h> #include <sys/wait.h> using namespace std; // compile: g++ Multihash.cpp -o Multihash // run: ./Multihash <file name> // parentToChild = pipe for parent to child communication // childToParent = pipe for child to parent communication int parentToChild[2]; int childToParent[2]; // names of the hash programs const char hashAlgs[6][10] = {"md5sum", "sha1sum", "sha224sum", "sha256sum", "sha384sum", "sha512sum"}; // read end of pipe const int READ_END = 0; // write end of pipe const int WRITE_END = 1; // number of hash programs const int NUM_HASH_ALGS = sizeof(hashAlgs)/sizeof(hashAlgs[0]); // maximum length of hash value const int HASH_LENGTH = 1000; // maximum length of file name const int FILENAME_LENGTH = 100; // child function which computes the hash of a file and // returns the value back to the parent // @param 'hashAlg' == the name of the hash program void ComputeHash(const char hashAlg[]); int main(int argc, char* argv[]) { // declare variables ifstream infile; // used to see if file exists char hashValue[HASH_LENGTH]; // hash value which child passes to parent // check to see if theres enough commandline args if(argc < 2) { cerr<<"n** ERROR - NOT ENOUGH ARGUMENTS!n" <<"nUSAGE: "<<argv[0]<<" <FILE NAME>n"; exit(1); } // try to open file infile.open(argv[1]); // if file doesnt exist, then exit if(infile.fail()) { cerr<<"n** ERROR!n" <<"Cant find the file ""<<argv[1]<<""!nn"; exit(1); } infile.close(); // start the piping process for(int currentAlg = 0; currentAlg < NUM_HASH_ALGS; ++currentAlg) { // create pipes if((pipe(parentToChild) < 0) || (pipe(childToParent) < 0)) { cerr << "npipe failedn"; exit(1); } // create & fork a child pid_t pid = fork(); // make sure fork succeeded if(pid < 0) { cerr << "nfork failedn"; exit(1); } // child process else if(pid == 0) { ComputeHash(hashAlgs[currentAlg]); } // parent process else { // close selected pipes ends close(parentToChild[READ_END]); close(childToParent[WRITE_END]); // pass the filename to the child write(parentToChild[WRITE_END], argv[1], strlen(argv[1])+1); close(parentToChild[WRITE_END]); // read the incoming hash value from the child read(childToParent[READ_END], hashValue, sizeof(hashValue)); close(childToParent[READ_END]); // wait for child to complete wait(NULL); // display current hash to the screen cout<<"Hash Algorithm #"<<currentAlg+1<<":n"<<hashAlgs[currentAlg] <<" - HASH VALUE: "<<hashValue<<endl; // reset hash buffer memset(hashValue, (char)NULL, sizeof(hashValue)); } } cerr<<"The parent process is now exiting...n"; return 0; }// end of main void ComputeHash(const char hashAlg[]) { // declare variables char recievedFileName[FILENAME_LENGTH]; // saves recieved filename char cmdLine[FILENAME_LENGTH]; // saves command line for popen char hashOutput[HASH_LENGTH]; // saves final hash output FILE* popenOutput; // popen file pointer // close selected pipes ends close(parentToChild[WRITE_END]); close(childToParent[READ_END]); // get filename from parent read(parentToChild[READ_END], recievedFileName, sizeof(recievedFileName)); close(parentToChild[READ_END]); // construct command line argument to pass to popen strncpy(cmdLine, hashAlg, sizeof(cmdLine)); strncat(cmdLine, " ", sizeof(cmdLine)); strncat(cmdLine, recievedFileName, sizeof(cmdLine)); // get has value for current hash prog popenOutput = popen(cmdLine, "r"); // make sure that popen succeeded if(!popenOutput) { cerr<<"npopen failedn"; exit(1); } // set hash buffer to all NULLS memset(hashOutput, (char)NULL, sizeof(hashOutput)); // read program output into buffer fread(hashOutput, sizeof(char), sizeof(char)*sizeof(hashOutput), popenOutput); // close popen buffer pclose(popenOutput); // pass hash value back to parent write(childToParent[WRITE_END], hashOutput, strlen(hashOutput)+1); close(childToParent[WRITE_END]); // exit 'AKA' kill child exit(0); }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
Using the following example input file located here, the following is sample output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Hash Algorithm #1: md5sum - HASH VALUE: cd2f02cc5e50bd08d83cef630a32d7d6 INPUT_Dictionary_programmingnotes_freeweq_com.txt Hash Algorithm #2: sha1sum - HASH VALUE: 1e1d2fd77b331fc2b2f24822fdf2133f3678d662 INPUT_Dictionary_programmingnotes_freeweq_com.txt Hash Algorithm #3: sha224sum - HASH VALUE: bf69da0f9f9990cece8dd6800a25a9faca5381c0c62e0667c258e8d5 INPUT_Dictionary_programmingnotes_freeweq_com.txt Hash Algorithm #4: sha256sum - HASH VALUE: a8cd7a082ce571f7e66bc4d5eea8f71e7e455735922c90c060580b51d43593ec INPUT_Dictionary_programmingnotes_freeweq_com.txt Hash Algorithm #5: sha384sum - HASH VALUE: a564df501470a43eebb51c28f83ab07090e39c434fc40bf496622f536c525140301633d6513dec83ea512bcb38c4e2e6 INPUT_Dictionary_programmingnotes_freeweq_com.txt Hash Algorithm #6: sha512sum - HASH VALUE: e686a709b3025f8b21fd6d40ce92f741dd8644335e935baced86098ff3e7278b5443e28f02fa886e7c0cd391ce6e8aa91842a6de29535a40453d41862a0bc1c7 INPUT_Dictionary_programmingnotes_freeweq_com.txt The parent process is now exiting... |
C++ || Snippet – How To Use Popen & Save Results Into A Character Array
The following is sample code which demonstrates the use of the “popen” function call on Unix based systems.
The “popen” function call opens a process by creating a pipe, forking, and invoking the shell. It does this by executing the command specified by the incoming string function parameter. It creates a pipe between the calling program and the executed command, and returns a pointer to a stream that can be used to either read from or write to the pipe.
The following example demonstrates how to save the results of the popen command into a char array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// ============================================================================ // Author: Kenneth Perkins // Date: Aug 20, 2013 // Taken From: http://programmingnotes.org/ // File: PopenExample.cpp // Description: Demonstrate the use of the popen() command and demonstrate // how to save the results into a char array. // ============================================================================ #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> using namespace std; // the maximum output size const int MAX_OUTPUT_SIZE = 1000; int main(int argc, char* argv[]) { // declare variables char buffer[MAX_OUTPUT_SIZE]; // a char array to display the popen output FILE* progOutput; // a file pointer representing the popen output // launch the "md5sum" program to compute the MD5 hash of // the file "/bin/ls" and save it into the file pointer progOutput = popen("md5sum /bin/ls", "r"); // make sure that popen succeeded if (!progOutput) { perror("popen"); exit(1); } // reset buffer to all NULLS memset(buffer, (char)NULL, sizeof(buffer)); // read the popen output into the char array buffer if (fread(buffer, sizeof(char), sizeof(char) * sizeof(buffer), progOutput) < 0) { perror("fread"); exit(1); } // close the file pointer representing the popen output if (pclose(progOutput) < 0) { perror("pclose"); exit(1); } cout << "Program output: " << buffer << endl; return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
Program output: fa97c59cc414e42d4e0e853ddf5b4745 /bin/ls
C++ || Serial & Parallel Multi Process File Downloader Using Fork & Execlp
The following is another homework assignment which was presented in an Operating Systems Concepts class. The following are two multi-process programs using commandline arguments, which demonstrates more practice using the fork() and execlp() system calls on Unix based systems.
==== 1. OVERVIEW ====
File downloaders are programs used for downloading files from the Internet. The following programs listed on this page implement two distinct type of multi-process downloaders:
1. a serial file downloader which downloads files one by one.
2. a parallel file downloader which dowloads multiple files in parallel.
In both programs, the parent process first reads a file via the commandline. This file which is read is the file that contains the list of URLs of the files to be downloaded. The incoming url file that is read has the following format:
[URL1]
[URL2]
.
.
.
[URLN]
Where [URL] is an http internet link with a valid absolute file path extension.
(i.e: http://newsimg.ngfiles.com/270000/270173_0204618900-cc-asmbash.jpg)
After the url file is parsed, next the parent process forks a child process. Each created child process uses the execlp() system call to replace its executable image with that of the “wget” program. The use of the wget program performs the actual file downloading.
==== 2. SERIAL DOWNLOADER ====
The serial downloader downloads files one at a time. After the parent process has read and parsed the incoming url file from the commandline, the serial downloader proceeds as follows:
1. The parent forks off a child process.
2. The child uses execlp("/usr/bin/wget", "wget", [URL STRING1], NULL) system call in order to replace its program with wget program that will download the first file in urls.txt (i.e. the file at URL).
3. The parent executes a wait() system call until the child exits.
4. The parent forks off another child which downloads the next file specified in url.txt.
5. Repeat the same process until all files are downloaded.
The following is implemented below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
// ============================================================================ // Author: Kenneth Perkins // Date: Aug 19, 2013 // Taken From: http://programmingnotes.org/ // File: Serial.cpp // Description: File downloaders are programs used for downloading files // from the Internet. Using the fork() & execlp("wget") command, the // following is a multi-process serial file downloader which reads an // input file containing url file download links as a commandline // argument and downloads the files located on the internet one by one. // ============================================================================ #include <iostream> #include <cstring> #include <fstream> #include <cstdlib> #include <unistd.h> #include <sys/wait.h> using namespace std; // compile & run // g++ Serial.cpp -o Serial // ./Serial urls.txt int main(int argc, char* argv[]) { // declare variables pid_t pid = -1; int urlNumber = 0; char urlName[256]; ifstream infile; // check if theres enough command line args if(argc < 2) { cout <<"\nERROR -- NOT ENOUGH ARGS!" <<"\n\nUSAGE: "<<argv[0]<<" <file containing url downloads>\n\n"; exit(1); } // try to open the file containing the download url links // exit if the url file is not found infile.open(argv[1]); if(infile.fail()) { cout <<"\nERROR -- "<<argv[1]<<" NOT FOUND!\n\n"; exit(1); } // get download url links from the file while(infile.getline(urlName, sizeof(urlName))) { ++urlNumber; // fork another process pid = fork(); if(pid < 0) { // ** error occurred perror("fork"); exit(1); } else if(pid == 0) { // ** child process cout <<endl<<"** URL #"<<urlNumber <<" is currently downloading... **\n\n"; execlp("/usr/bin/wget", "wget", urlName, NULL); } else { // ** parent process // parent will wait for the child to complete wait(NULL); cout <<endl<<"-- URL #"<<urlNumber<<" is complete! --\n"; } } infile.close(); cout <<endl<<"The parent process is now exiting...\n"; return 0; }// http://programmingnotes.org/ |
Since the serial downloader downloads files one at a time, that can become very slow. That is where the parallel downloader comes in handy!
==== 3. PARALLEL DOWNLOADER ====
The parallel downloader downloads files all at once and is implemented much like the serial downloader. The parallel downloader proceeds as follows:
1. The parent forks off n children, where n is the number of URLs in url.txt.
2. Each child executes execlp("/usr/bin/wget", "wget", [URL STRING], NULL) system call where eachis a distinct URL in url.txt.
3. The parent calls wait() (n times in a row) and waits for all children to terminate.
4. The parent exits.
The following is implemented below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
// ============================================================================ // Author: Kenneth Perkins // Date: Aug 19, 2013 // Taken From: http://programmingnotes.org/ // File: Parallel.cpp // Description: File downloaders are programs used for downloading files // from the Internet. Using the fork() & execlp("wget") command, the // following is a multi-process parallel file downloader which reads an // input file containing url file download links as a commandline // argument and downloads the files located on the internet all at once. // ============================================================================ #include <iostream> #include <cstring> #include <fstream> #include <cstdlib> #include <unistd.h> #include <sys/wait.h> using namespace std; // compile & run // g++ Parallel.cpp -o Parallel // ./Parallel urls.txt int main(int argc, char* argv[]) { // declare variables pid_t pid = -1; int urlNumber = 0; char urlName[256]; ifstream infile; // check if theres enough command line args if(argc < 2) { cout <<"\nERROR -- NOT ENOUGH ARGS!" <<"\n\nUSAGE: "<<argv[0]<<" <file containing url downloads>\n\n"; exit(1); } // try to open the file containing the download url links // exit if the url file is not found infile.open(argv[1]); if(infile.fail()) { cout <<"\nERROR -- "<<argv[1]<<" NOT FOUND!\n\n"; exit(1); } // get download url links from the file while(infile.getline(urlName, sizeof(urlName))) { ++urlNumber; // fork another process pid = fork(); if(pid < 0) { // ** error occurred perror("fork"); exit(1); } else if(pid == 0) { // ** child process cout <<endl<<"** URL #"<<urlNumber <<" is currently downloading... **\n\n"; execlp("/usr/bin/wget", "wget", urlName, NULL); } } infile.close(); while(urlNumber > 0) { // ** parent process // parent will wait for the child to complete wait(NULL); cout <<endl<<"-- URL #"<<urlNumber<<" is complete! --\n"; --urlNumber; } cout <<endl<<"The parent process is now exiting...\n"; return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
Also note, while the parallel downloader executes, the outputs from different children may intermingle.
C++ || Snippet – How To Use Fork & Pipe For Interprocess Communication
The following is sample code which demonstrates the use of the fork, read, and write function calls for use with pipes on Unix based systems.
A pipe is a mechanism for interprocess communication. Data written to a pipe by one process can be read by another process. Creating a pipe is achieved by using the pipe function, which creates both the reading and writing ends of the pipe file descriptor.
In typical use, a parent process creates a pipe just before it forks one or more child processes. The pipe is then used for communication between either the parent or child processes, or between two sibling processes.
A real world example of this kind of communication can be seen in all operating system terminal shells. When you type a command in a shell, it will spawn the executable represented by that command with a call to fork. A pipe is opened to the new child process, and its output is read and printed by the terminal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
// ============================================================================ // Author: Kenneth Perkins // Date: Aug 19, 2013 // Taken From: http://programmingnotes.org/ // File: ForkPipeExample.cpp // Description: Demonstrate the use of the fork() and pipe() command for // parent and child interprocess communication. // ============================================================================ #include <iostream> #include <cstdlib> #include <cstring> #include <sys/wait.h> #include <unistd.h> using namespace std; // global variables const int BUFFER_SIZE = 256; const int READ_END = 0; const int WRITE_END = 1; int main() { // declare variables int fd[2]; // pipe file descriptor pid_t pid = -1; char writeMsg[BUFFER_SIZE] = "Greetings From Your Parent!"; char readMsg[BUFFER_SIZE]; // create a pipe if(pipe(fd) < 0) { perror("pipe"); exit(1); } cout <<"\nParent is forking a child." << endl; // create a duplicate process of this current program pid = fork(); // exit if something went wrong with the fork if(pid < 0) { perror("fork"); exit(1); } // this code only gets executed by the child process else if(pid == 0) { cout <<"\nStarting the child process..\n\n"; // close the "write" end b/c we arent using it close(fd[WRITE_END]); // get data from the pipe using the read() function read(fd[READ_END], readMsg, sizeof(readMsg)); // close the "read" end close(fd[READ_END]); cout <<"Message from the parent via the pipe: "<<readMsg<<endl; } // the parent process executes here else { cout <<"\nParent is now waiting for child id #"<<pid<<" to complete..\n"; // close the "read" end b/c we arent using it close(fd[READ_END]); // write data on pipe write(fd[WRITE_END], writeMsg, strlen(writeMsg)+1); // close the "write" end close(fd[WRITE_END]); // use the "wait" function to wait for the child to complete wait(NULL); cout <<"\nThe child process is complete and has terminated!\n"; } // NOTE: this message gets displayed twice - Why?! cout <<"\nProgram is now exiting...\n"; return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
Parent is forking a child.
Parent is now waiting for child id #12776 to complete..Starting the child process..
Message from the parent via the pipe: Greetings From Your Parent!
Program is now exiting...
The child process is complete and has terminated!
Program is now exiting...
C++ || Snippet – How To Use Fork & Execlp For Interprocess Communication
The following is sample code which demonstrates the use of the “fork” and “execlp” function calls on Unix based systems.
The “fork” function call creates a new process by duplicating the calling process; or in more simpler terms, it creates a duplicate process (a child) of the calling (parent) process.
This new process, referred to as the child, is an exact duplicate of the calling process, referred to as the parent.
The “execlp” function call is part of a family of functions which replaces a current running process image with a new process image. That means by using the “execlp” function call, you are basically replacing the entire current running process with a new user defined program.
Though fork and execlp are not required to be used together, they are often used in conjunction with one another as a way of creating a new program running as a child of another process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
// ============================================================================ // Author: Kenneth Perkins // Date: Aug 19, 2013 // Taken From: http://programmingnotes.org/ // File: ForkExample.cpp // Description: Demonstrate the use of the fork() command for parent and // child process manipulation. // ============================================================================ #include <iostream> #include <cstdlib> #include <sys/wait.h> #include <unistd.h> using namespace std; int main() { // declare variable pid_t pid = -1; cout <<"\nParent is forking a child." << endl; // create a duplicate process of this current program pid = fork(); // exit if something went wrong with the fork if(pid < 0) { perror("fork"); exit(1); } // this code only gets executed by the child process else if(pid == 0) { cout <<"\nStarting the child process..\n" << endl; // NOTE: the "execlp" function replaces the current // child process with the terminal "ls" command. // the child process finally dies after the "execlp" // function is complete execlp("/bin/ls", "ls", "-l", NULL); // "ls -l" command cout <<"\nNOTE: This never gets executed - Why?!"; } // the parent process executes here else { cout <<"\nParent is now waiting for child id #"<<pid<<" to complete.." << endl; // use the "wait" function to wait for the child to complete wait(NULL); cout <<"\nThe child process is complete and has terminated!\n"; } cout <<"\nParent is now exiting...\n"; return 0; }// http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
The following is sample output:
Parent is forking a child.
Parent is now waiting for child id #10872 to complete..Starting the child process..
total 1466
-rw-r--r-- 1 admin admin 468 Apr 27 2012 nautilus-computer.desktop
-rwxrwxr-x 1 admin admin 9190 Aug 19 15:17 ForkExample
-rw-rw-r-- 1 admin admin 1640 Aug 19 15:17 ForkExample.cppThe child process is complete and has terminated!
Parent is now exiting...