进程篇(3: 基本进程控制:进程的创建)--请参照本博客“操作系统”专栏
1. 进程标识符:
每个进程都有一个非负整型表示的唯一进程ID。但进程ID可以重用,当一个进程终止之后,其进程ID就可以再次被重用了。
UNIX系统中常常有一些专用的进程:
- ID为0的进程通常是调度进程,常常被称为交换进程(swapper),该进程是内核的一部分,它并不执行磁盘上的任何程序,因此也被称为系统进程。
- ID为1的进程通常是init进程,在自举过程结束后由内核调用,在比较新的版本中是/sbin/init。此进程负责在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件,并将系统引导到一个状态!
- ID为2的进程是页守护进程,此进程负责支持虚拟存储系统的分页操作。
Unix中返回一些与进程相关的标识符的函数:
1) 返回调用进程及其父进程的ID:
1 SYNOPSIS 2 #include <sys/types.h> 3 #include <unistd.h> 4 5 pid_t getpid(void); 6 pid_t getppid(void); 7 8 DESCRIPTION 9 getpid() returns the process ID of the calling process. (This is often used by routines that generate 10 unique temporary filenames.) 11 12 getppid() returns the process ID of the parent of the calling process. 13 14 ERRORS 15 These functions are always successful.
下面我们来编写一段程序返回一个程序的调用进程的id号:
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <unistd.h> 4 5 int main(void) 6 { 7 pid_t calling_id = getpid(); 8 printf("The calling process id: %d\n",calling_id); 9 return 0; 10 }
我在我的计算机上运行./a.out 三次,结果如下:
1 The calling process id: 5539 2 The calling process id: 5542 3 The calling process id: 5543
可见getpid返回的是调用 pid_t calling_id = getpid(); 这段代码的进程的进程id号。因为每次调用都不会重用上一次的进程id号,所以呈现递增的趋势!
下面我们编一段代码返回调用进程的父进程id号:
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <unistd.h> 4 5 int main(void) 6 { 7 pid_t pp_id = getppid(); 8 printf("The parent id of calling process: %d\n",pp_id); 9 return 0; 10 }
我们在shell中连续运行3次./a.out,结果如下:
The parent id of calling process: 5088 The parent id of calling process: 5088 The parent id of calling process: 5088
我们用ps命令查看当前用户系统中运行的程序id:
PID TTY TIME CMD 2651 pts/0 00:00:00 bash 5088 pts/0 00:00:00 bash 5670 pts/0 00:00:00 ps
显然这个pp_id就是当前运行a.outd的shell进程的id。
2)返回调用程序的用户及有效用户id:
1 SYNOPSIS 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 uid_t getuid(void); 6 uid_t geteuid(void); 7 8 DESCRIPTION 9 getuid() returns the real user ID of the calling process. 10 11 geteuid() returns the effective user ID of the calling process. 12 13 ERRORS 14 These functions are always successful.
3) 返回进程的组id和有效组id:
1 SYNOPSIS 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 gid_t getgid(void); 6 gid_t getegid(void); 7 8 DESCRIPTION 9 getgid() returns the real group ID of the calling process. 10 11 getegid() returns the effective group ID of the calling process. 12 13 ERRORS 14 These functions are always successful.
2. 进程的创建,执行和结束终止:
2.1 进程创建:
2.1.1 fork 函数:
NAME fork - create a child process(创建一个子进程) SYNOPSIS #include <unistd.h> pid_t fork(void); DESCRIPTION fork() creates a new process by duplicating the calling process(复制调用进程来创建新进程). The new process, referred to as the child, is an exact duplicate of the calling process, referred to as the parent, except for the following points: * The child has its own unique process ID, and this PID does not match the ID of any exist‐ ing process group (setpgid(2)). * The child's parent process ID is the same as the parent's process ID. * The child does not inherit its parent's memory locks (mlock(2), mlockall(2)).(子进程并不继承父进程的内存锁) * Process resource utilizations (getrusage(2)) and CPU time counters (times(2)) are reset to zero in the child.(子进程的资源使用计数和CPU时间计数都被值成0) * The child's set of pending signals is initially empty (sigpending(2)).(子进程的挂起信号量的数目初始化为0) * The child does not inherit semaphore adjustments from its parent (semop(2)).(子进程并不继承父进程的信号量调节器) * The child does not inherit record locks from its parent (fcntl(2)).(子进程并不继承父进程的记录锁) * The child does not inherit timers from its parent (setitimer(2), alarm(2), timer_cre‐ ate(2)).(子进程不从父进程中继承时间计数器) * The child does not inherit outstanding asynchronous I/O operations from its parent (aio_read(3), aio_write(3)), nor does it inherit any asynchronous I/O contexts from its parent (see io_setup(2)). (子进程并不继承父进程的异步I/O操作和异步I/O内容) The process attributes in the preceding list are all specified in POSIX.1-2001. The parent and child also differ with respect to the following Linux-specific process attributes: * The child does not inherit directory change notifications (dnotify) from its parent (see the description of F_NOTIFY in fcntl(2)). * The prctl(2) PR_SET_PDEATHSIG setting is reset so that the child does not receive a sig‐ nal when its parent terminates. * The default timer slack value is set to the parent's current timer slack value. See the description of PR_SET_TIMERSLACK in prctl(2). * Memory mappings that have been marked with the madvise(2) MADV_DONTFORK flag are not inherited across a fork(). * The termination signal of the child is always SIGCHLD (see clone(2)). * The port access permission bits set by ioperm(2) are not inherited by the child; the child must turn on any bits that it requires using ioperm(2). Note the following further points: * The child process is created with a single thread—the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause. * The child inherits copies of the parent's set of open file descriptors. Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent. This means that the two descriptors share open file status flags, current file offset, and signal-driven I/O attributes (see the description of F_SETOWN and F_SETSIG in fcntl(2)). * The child inherits copies of the parent's set of open message queue descriptors (see mq_overview(7)). Each descriptor in the child refers to the same open message queue description as the corresponding descriptor in the parent. This means that the two descriptors share the same flags (mq_flags). * The child inherits copies of the parent's set of open directory streams (see opendir(3)). POSIX.1-2001 says that the corresponding directory streams in the parent and child may share the directory stream positioning; on Linux/glibc they do not. RETURN VALUE 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, and errno is set appropriately.(返回值) ERRORS EAGAIN fork() cannot allocate sufficient memory to copy the parent's page tables and allo‐ cate a task structure for the child. EAGAIN It was not possible to create a new process because the caller's RLIMIT_NPROC resource limit was encountered. To exceed this limit, the process must have either the CAP_SYS_ADMIN or the CAP_SYS_RESOURCE capability. ENOMEM fork() failed to allocate the necessary kernel structures because memory is tight. ENOSYS fork() is not supported on this platform (for example, hardware without a Memory-Man‐ agement Unit). CONFORMING TO SVr4, 4.3BSD, POSIX.1-2001. NOTES Under Linux, fork() is implemented using copy-on-write pages, so the only penalty that it incurs is the time and memory required to duplicate the parent's page tables, and to create a unique task structure for the child. Since version 2.3.3, rather than invoking the kernel's fork() system call, the glibc fork() wrapper that is provided as part of the NPTL threading implementation invokes clone(2) with flags that provide the same effect as the traditional system call. (A call to fork() is equivalent to a call to clone(2) specifying flags as just SIGCHLD.) The glibc wrapper invokes any fork handlers that have been established using pthread_atfork(3).
下面一段程序演示了fork函数,可以看出,子进程对变量所做的改变并不影响父进程中变量的值!
1 #include "apue.h" 2 3 int glob = 6; /* external variable in initialized data */ 4 char buf[] = "a write to stdout\n"; 5 6 int 7 main(void) 8 { 9 int var; /* automatic variable on the stack */ 10 pid_t pid; 11 12 var = 88; 13 if(write(STDOUT_FILENO,buf,sizeof(buf)-1) != sizeof(buf)-1) 14 err_sys("write error"); 15 printf("before fork\n"); 16 17 if((pid = fork()) < 0) 18 { 19 err_sys("fork error"); 20 } 21 else if(pid == 0) /* child! */ 22 { 23 glob++; 24 var++; 25 } 26 else 27 sleep(2); 28 29 printf("pid = %d, glob = %d, var = %d\n",getpid(),glob,var); 30 exit(0); 31 }
$ ./a.out > result
a write to stdout before fork pid = 6275, glob = 7, var = 89 before fork pid = 6274, glob = 6, var = 88
一般来说,在调用fork函数之后,父进程和子进程的执行顺序是不确定的,这取决于内核所使用的进程调度算法!
如果要实现父子进程间的的同步,必须实现进程间的通信,这里,让父进程sleep两秒而让子进程先执行!
3. 父子进程间文件的共享:
观察到上述程序的一个特质:在重定向父进程的标准输出时,子进程的标准输出也被重定向!实际上fork函数的一个特征是:
父进程所有打开的文件描述符都被复制到子进程中,父子进程的每个相同的打开描述符共享一个文件表项。

上图显示了fork之后父,子进程间对打开文件的共享。
在fork之后处理文件描述符有常见的两种方法:
- 父进程等待子进程的完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读,写操作的任一共享描述符的文件偏移量已经执行了相应的更新。
- 父子进程各自执行不同的程序段。在这种情况下,在fork之后,父子进程各自关闭他们不需要使用的文件描述符,这样就不会干扰对方使用文件描述符。这种情形常见于网络服务进程中。
fork有下面的常见两种用法:
1). 一个进程希望复制自己,使得父子进程执行不同的代码段。在网络服务程序中这种情况最为常见: 父进程等待客户端的请求,当一个请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个进程的到来!
2). 一个进程要执行一个不同的程序。这对shell来所是一个常见的情形. 在这种情况下,子进程创建之后立即调用exec。
下面我们通过一个实例程序来说明父子进程之间的文件共享问题:
1 #include "apue.h" 2 3 int 4 main(void) 5 { 6 FILE *pf = fopen("Test.txt","r+"); 7 pid_t pid; 8 9 char buf[] = "This is the father process!\n"; 10 int nch = sizeof(buf) - 1; 11 12 if(pf != NULL) 13 { 14 fwrite(buf,sizeof(char),nch,pf); 15 16 if((pid = fork()) < 0) 17 { 18 err_sys("fork error!"); 19 } 20 else if(pid == 0) 21 { 22 char buf[] = "This is the son process!\n"; 23 int nch = sizeof(buf) - 1; 24 25 fwrite(buf,sizeof(char),nch,pf); 26 } 27 else 28 sleep(2); 29 } 30 exit(0); 31 }
上面这段程序的在Test.txt文件中的输出结果是:
This is the father process! This is the son process! This is the father process!
为什么有两 “This is the father process!” ,因为父子进程将共享I/O缓冲区,父进程的缓冲区的内容会被复制到子进程的缓冲区中。
我们将缓冲区冲洗一下:
1 #include "apue.h" 2 3 int 4 main(void) 5 { 6 FILE *pf = fopen("Test.txt","r+"); 7 pid_t pid; 8 9 char buf[] = "This is the father process!\n"; 10 int nch = sizeof(buf) - 1; 11 12 if(pf != NULL) 13 { 14 fwrite(buf,sizeof(char),nch,pf); 15 16 if(fflush(pf) == EOF) 17 err_sys("file flush error!"); 18 19 if((pid = fork()) < 0) 20 { 21 err_sys("fork error!"); 22 } 23 else if(pid == 0) 24 { 25 char buf[] = "This is the son process!\n"; 26 int nch = sizeof(buf) - 1; 27 28 fwrite(buf,sizeof(char),nch,pf); 29 } 30 else 31 sleep(2); 32 } 33 exit(0); 34 }
结果变成了:
This is the father process! This is the son process!
3. vfork 函数:
vfork 函数的返回值和调用序列和fork函数相同,但语义不同. 下面是linux manpage 中对vfork函数的描述:
NAME vfork - create a child process and block parent(注意这里与fork函数的差别:创建了父进程之后阻塞父进程) SYNOPSIS #include <sys/types.h> #include <unistd.h> pid_t vfork(void); Feature Test Macro Requirements for glibc (see feature_test_macros(7)): vfork(): Since glibc 2.12: _BSD_SOURCE || (_XOPEN_SOURCE >= 500 || _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED) && !(_POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700) Before glibc 2.12: _BSD_SOURCE || _XOPEN_SOURCE >= 500 || _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED DESCRIPTION Standard description (From POSIX.1) The vfork() function has the same effect as fork(2), except that the behavior is undefined if the process created by vfork() either modifies any data other than a variable of type pid_t used to store the return value from vfork(), or returns from the function in which vfork() was called, or calls any other function before success‐ fully calling _exit(2) or one of the exec(3) family of functions. Linux description vfork(), just like fork(2), creates a child process of the calling process. For details and return value and errors, see fork(2). vfork() is a special case of clone(2). It is used to create new pro‐ cesses without copying the page tables of the parent process. It may be useful in performance-sensitive applications where a child is cre‐ ated which then immediately issues an execve(2). vfork() differs from fork(2) in that the calling thread is suspended until the child terminates (either normally, by calling _exit(2), or abnormally, after delivery of a fatal signal), or it makes a call to execve(2). Until that point, the child shares all memory with its par‐ ent, including the stack. The child must not return from the current function or call exit(3), but may call _exit(2). As with fork(2), the child process created by vfork() inherits copies of various of the caller's process attributes (e.g., file descriptors, signal dispositions, and current working directory); the vfork() call differs only in the treatment of the virtual address space, as described above. Signals sent to the parent arrive after the child releases the parent's memory (i.e., after the child terminates or calls execve(2)).
vfork和fork之间的却别在于:
- vfork创建的新进程主要用来执行一个新程序!并且并不将父进程的地址空间完全复制到子进程的地址空间中去。
- vfork保证了子进程的优先运行,当子进程调用了exec或exit函数之后,父进程才可以得到执行!(如果在调用这两个函数之前还要依赖父进程的动作,就会导致死锁的发生)。
下面使用vfork创建一个子进程:
1 #include "apue.h" 2 3 int glob = 6; /* external variable in initialized data */ 4 5 int 6 main(void) 7 { 8 int var; /* automatic variable on the stack */ 9 pid_t pid; 10 11 var = 88; 12 printf("before vfork\n"); /* we do not flush stdio */ 13 if((pid = vfork()) < 0) 14 { 15 err_sys("vfork error!"); 16 } 17 else if(pid == 0) 18 { 19 glob++; 20 var++; 21 _exit(0); 22 } 23 24 /* 25 * Parent continues here 26 */ 27 printf("pid = %d, glob = %d, var = %d\n",getpid(),glob,var); 28 exit(0); 29 }
./a.out 运行结果:
before vfork pid = 16261, glob = 7, var = 89
posted on 2014-05-31 21:06 Dream Catcher(DC) 阅读(378) 评论(0) 收藏 举报
浙公网安备 33010602011771号