进程篇(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)    收藏  举报

导航