欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

024 UNIX再学习 -- 进程关系

程序员文章站 2022-05-02 16:32:07
...
                                          
                                              024 UNIX再学习 -- 进程关系                                      
                                                                                
                                           

APUE 第 10 章信号讲完,回过头来看一下第 9 章的进程关系。终端登录和网络登录部分,我们只讲 Linux 系统的。

一、终端登录

我记得我们讲 root 登录设置时有提到,参看:C语言再学习 -- Ubuntu 12.04 root用户登录设置
其中,使用Ctrl+Alt+F1 进入纯命令模式,重新依据上面的设置,重启 OK!

Ctrl+Alt切换到Windows

Ctrl+Alt+F7 退出纯命令模式。


我们来看一下,进入纯命令模式界面:

024 UNIX再学习 -- 进程关系


第二种方法也可以通过 Ctrl+Alt+T 打开终端。
024 UNIX再学习 -- 进程关系

那什么是 Linux 的终端登录?
1、当系统自举时,内核创建进程 ID 为 1 的进程,也就是 init 进程。init 进程使系统进入多用户模式。init 进程根据配置文件 /etc/inittab 确定需要打开哪些终端,对每一个允许登录的终端设备,init 调用一次 fork,它所生成的子进程则执行 getty(exec)程序。(不同操作系统配置文件可能不同)。
2.getty 为终端设备调用 open 函数,以读写方式将终端打开。然后 getty 输出“longin:”之类的信息,并等待用户键入用户名。
3.当用户键入用户名后,getty 工作完成。然后调用 login 程序:
execle(“/bin/login”,”login”,”-p”,username,(char *)0,envp)
4.密码验证无误后,login 将切换目录到用户的 home 目录,改变该终端设备的权限,login 进程改变为登录用户 ID 并调用改用户的登录 shell:execl(“/bin/sh”,”-sh”,(char *)0)
5.登录shell读取其启动文件 (Bourne shell) 和 Korn shell。
从 getty 开始 exec 到 login,再 exec 到 bash,其实都是同一个进程,因此控制终端没变,文件描述符 0、1、2 也仍然指向控制终端。由于 fork 会复制 PCB 信息,所以由 Shell 启动的其它进程也都是如此。

人家讲的比我详细多了。。

二、网络登录

linux 网络登录使用扩展的因特网服务守护进程 xinetd,它等待大多数网路连接。
作为系统启动的一部分,init 调用一个 shell,使其执行 shell 脚本 /etc/rc。由此 shell 脚本启动一个守护进程
 xinetd。一旦此 shell 脚本终止,xinetd 的父进程就变成 init。xinetd 等待 TCP/IP 连接请求到达主机,而当一个连接请求到达时,它执行一次 fork,然后生成的子进程 exec 适当的程序。
就比如我们之前有讲过的 telnet 远程控制,参看:Hi3516A开发--环境搭建工具
024 UNIX再学习 -- 进程关系
重点是,当通过终端或网络登录时,我们得到了一个登录 shell,其标准输入、标准输出和标准错误要么连接一个终端设备,要么连接一个伪终端设备上。

三、进程组 ID

我们之前讲过进程 ID、父进程 ID。参看:UNIX再学习 -- 函数 fork 和 vfork
  1. #include <sys/types.h> 
  2. #include <unistd.h> 
  3. pid_t getpid(void); 返回:调用进程的进程 ID 
  4. pid_t getppid(void); 返回:调用进程的父进程 ID 
  5. uid_t getuid(void); 返回:调用进程的实际用户 ID 
  6. uid_t geteuid(void); 返回:调用进程的有效用户 ID 
  7. gid_t getgid(void); 返回:调用进程的实际组 ID 
  8. gid_t getegid(void); 返回:调用进程的有效组 ID 
  9. 注意,这些函数都没有出错返回 
(1)示例说明
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <sys/types.h> 
  4.  
  5. int main (void) 
  6.     printf ("pid = %d\n", getpid ()); 
  7.     printf ("ppid = %d\n", getppid ()); 
  8.     printf ("uid = %d\n", getuid ()); 
  9.     printf ("euid = %d\n", geteuid ()); 
  10.     printf ("gid = %d\n", getgid ()); 
  11.     printf ("egid = %d\n", getegid ()); 
  12.     return 0
  13. 输出结果: 
  14. pid = 3028 
  15. ppid = 2808 
  16. uid = 0 
  17. euid = 0 
  18. gid = 0 
  19. egid = 0 
  20. //每次执行结果都不一定相同 

每个进程除了有一个进程 ID 之外,还属于一个进程组。我们讲信号的时候,有提到过。那时不知道啥意思。
进程组是一个或多个进程的集合。通常,它们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号。每个进程组有一个唯一的进程组 ID。进程组 ID 类似于进程 ID — 它是一个正整数,并存放在 pid_t 数据类型中。函数 getpgrp 返回调用进程的进程组 ID。
  1. #include <unistd.h>
  2.        int setpgid(pid_t pid, pid_t pgid);
  3.        pid_t getpgid(pid_t pid);
  4.        pid_t getpgrp(void);                 /* POSIX.1 version */
  5.        pid_t getpgrp(pid_t pid);            /* BSD version */
  6.        int setpgrp(void);                   /* System V version */
  7.        int setpgrp(pid_t pid, pid_t pgid)/* BSD version */
可以看出系统不同用的函数也不同,建议使用 POSIX.1 规定中的无参数 getprgp() 函数
示例说明:
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <sys/types.h> 
  4.  
  5. int main (void) 
  6.     printf ("pid = %d\n", getpid ()); 
  7.     printf ("ppid = %d\n", getppid ()); 
  8.     printf ("uid = %d\n", getuid ()); 
  9.     printf ("euid = %d\n", geteuid ()); 
  10.     printf ("gid = %d\n", getgid ()); 
  11.     printf ("egid = %d\n", getegid ()); 
  12.  printf ("-----------------\n");
  13.  printf ("pgrp = %d\n", getpgrp ());
  14.     return 0
  15. }
  16. 输出结果:
  17. pid = 3497
  18. ppid = 3401
  19. uid = 0
  20. euid = 0
  21. gid = 0
  22. egid = 0
  23. -----------------
  24. pgrp = 3497
每个进程组有一个组长进程。组长进程的进程组 ID 等于其进程 ID
进程组组长可以创建一个进程组,常见该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命期。某个进程组中的最后一个进程可以终止,也可以转移到另一个进程组。

进程调用 setpgid 可以加入一个现有的进程组或者创建一个新进程组。
setpgid 函数将 pid 进程的进程组 ID 设置为 pgid。如果这两个参数相等,则由 pid 指定的进程编程进程组组长。如果 pid 是 0,则使用调用者的进程 ID。另外,如果 pgid 是 0,则由 pid 指定的进程 ID 用作进程组 ID。
示例说明:
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <sys/types.h> 
  4. #include <stdlib.h>
  5.  
  6. int main (void) 
  7.     printf ("pid = %d\n", getpid ()); 
  8.     printf ("ppid = %d\n", getppid ()); 
  9.     printf ("uid = %d\n", getuid ()); 
  10.     printf ("euid = %d\n", geteuid ()); 
  11.     printf ("gid = %d\n", getgid ()); 
  12.     printf ("egid = %d\n", getegid ()); 
  13.  printf ("-----------------\n");
  14.  printf ("pgrp = %d\n", getpgrp ());
  15.  if (-1 == (setpgid (0, getppid ())))
  16.   perror ("setpgid"), exit (1);
  17.  printf ("-----------------\n");
  18.  printf ("pgrp = %d\n", getpgrp ());
  19.     return 0
  20. }
  21. 输出结果:
  22. pid = 3593
  23. ppid = 3401
  24. uid = 0
  25. euid = 0
  26. gid = 0
  27. egid = 0
  28. -----------------
  29. pgrp = 3593
  30. -----------------
  31. pgrp = 3401

四、 会话

会话(session)是一个或多个进程组的集合。
举个例子:
024 UNIX再学习 -- 进程关系
如上例中,在一个会话中有 3 个进程组。通常是由 shell 的管道将几个进程编程一组的。

1、进程调用 setsid 函数建立一个新会话。

  1. #include <unistd.h>
  2. pid_t setsid(void);
  3. 返回值:成功返回进程组 ID;失败返回 -1

函数解析:

如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。具体会发生以下 3 件事。
(1)该进程变成新会话的会话首进程(会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。
(2)该进程成为一个新进程组的组长进程。新进程组 ID 是该调用进程的进程 ID。
(3)该进程没有控制终端。如果在调用 setsid 之前进程有一个控制终端,那么这种联系也被切断。

示例说明:

  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <sys/types.h> 
  4. #include <stdlib.h>
  5.  
  6. int main (void) 
  7.     printf ("pid = %d\n", getpid ()); 
  8.     printf ("ppid = %d\n", getppid ()); 
  9.     printf ("uid = %d\n", getuid ()); 
  10.     printf ("euid = %d\n", geteuid ()); 
  11.     printf ("gid = %d\n", getgid ()); 
  12.     printf ("egid = %d\n", getegid ()); 
  13.  printf ("-----------------\n");
  14.  printf ("pgrp = %d\n", getpgrp ());
  15. /*
  16.  if (-1 == (setpgid (0, getppid ())))
  17.   perror ("setpgid"), exit (1);
  18.  printf ("-----------------\n");
  19.  printf ("pgrp = %d\n", getpgrp ());
  20.  */
  21.      if(setsid() == -1)
  22.   perror ("setsig"), exit (1);
  23.     return 0
  24. }
  25. 输出结果:
  26. pid = 3738
  27. ppid = 3401
  28. uid = 0
  29. euid = 0
  30. gid = 0
  31. egid = 0
  32. -----------------
  33. pgrp = 3738
  34. setsig: Operation not permitted

示例解析:

该示例中进程组长 ID 就是进程 ID。看一下条件,如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。那 setsig 肯定出错了。
如果将注释去掉,改变它的进程组 ID,则 setsig 会建立新会话。且返回进程组 ID。
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <sys/types.h> 
  4. #include <stdlib.h>
  5.  
  6. int main (void) 
  7.     printf ("pid = %d\n", getpid ()); 
  8.     printf ("ppid = %d\n", getppid ()); 
  9.     printf ("uid = %d\n", getuid ()); 
  10.     printf ("euid = %d\n", geteuid ()); 
  11.     printf ("gid = %d\n", getgid ()); 
  12.     printf ("egid = %d\n", getegid ()); 
  13.  printf ("-----------------\n");
  14.  printf ("pgrp = %d\n", getpgrp ());
  15.  if (-1 == (setpgid (0, getppid ())))
  16.   perror ("setpgid"), exit (1);
  17.  printf ("-----------------\n");
  18.  printf ("pgrp = %d\n", getpgrp ());
  19.  pid_t sig = setsid ();
  20.  if(sig == -1)
  21.   perror ("setsig"), exit (1);
  22.  printf ("sig = %d\n", sig);
  23.     return 0
  24. }
  25. 输出结果:
  26. pid = 3771
  27. ppid = 3401
  28. uid = 0
  29. euid = 0
  30. gid = 0
  31. egid = 0
  32. -----------------
  33. pgrp = 3771
  34. -----------------
  35. pgrp = 3401
  36. sig = 3771

2、getsid 函数返回会话首进程的进程 ID

  1. #include <unistd.h>
  2. pid_t getsid(pid_t pid);
  3. 返回值:成功返回会话首进程的进程组 ID;失败返回 -1

(1)函数解析

如若 pid 是 0,getsid 返回调用进程的会话首进程的进程组 ID。
出于安全方面的的考虑,一些实现有如下限制:如若 pid 并不属于调用者所在的会话,那么调用进程就不能得到该会话首进程的进程组 ID。

(2)示例说明

  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <sys/types.h> 
  4. #include <stdlib.h>
  5.  
  6. int main (void) 
  7.     printf ("pid = %d\n", getpid ()); 
  8.     printf ("ppid = %d\n", getppid ()); 
  9.     printf ("uid = %d\n", getuid ()); 
  10.     printf ("euid = %d\n", geteuid ()); 
  11.     printf ("gid = %d\n", getgid ()); 
  12.     printf ("egid = %d\n", getegid ()); 
  13.  printf ("-----------------\n");
  14.  printf ("pgrp = %d\n", getpgrp ());
  15.         //pid 为 0
  16.  printf ("sid = %d\n", getsid (0));
  17.     return 0
  18. }
  19. 输出结果:
  20. pid = 3809
  21. ppid = 3401
  22. uid = 0
  23. euid = 0
  24. gid = 0
  25. egid = 0
  26. -----------------
  27. pgrp = 3809
  28. sid = 3401

五、控制终端

一个会话可以有一个控制终端。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。
建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。
如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
无论何时键入终端的中断键(常常是 Delete 或 Ctrl+C),都会将中断信号发送至前台进程组的所有进程。
无论何时键入终端的退出键(常常是 Ctrl+\),都会将退出信号发送至前台进程组的所有进程。
如果终端接口检测调制解调器(或网线)已经断开连接,则将挂断信号发送至控制进程(会话首进程)。
通常我们不必担心控制终端,登录时,将自动建立控制终端。
024 UNIX再学习 -- 进程关系

六、作业控制

作业控制是 BSD 在 1980 年左右增加的一个特性。它允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业在后台运行。作业控制要求以下 3 种形式的支持。
(1)支持作业控制的 shell
(2)内核中的终端驱动程序必须支持作业控制
(3)内核必须提供对某些作业控制信号的支持

后台运行是用 ‘&’比如:   #./a.out &   
比如 shell 脚本里使用,./thttpd.sh &  
实际上有 3 个特殊字符可使终端驱动程序产生信号,并将它们发送至前台进程组,它们是:
中断字符(一般采用Delet 或 Ctrl+C)产生 SIGINT
退出字符(一般采用Ctrl+\)产生 SIGQUIT
挂起字符(一般采用 Ctrl+Z)产生 SIGTSTP
可以用 bg 使其后台继续运行,fg 使其转入前台运行。

七、shell 执行程序

让我们检验一下 shell 是如何执行程序的,以及这与进程组、控制终端和会话等概念的关系。
shell 编程有讲,参看:UNIX再学习 -- shell编程
使用 ps 命令,首先使用不支持作业控制的、在 Solaris 上运行的经典 Bourne shell。
  1. #include <stdio.h>   
  2. #include <stdlib.h>   
  3. #include <unistd.h>   
  4.  
  5. int main (void)   
  6. {   
  7.     pid_t pid, pr;   
  8.    
  9.     pid = fork ();   
  10.     if  (pid == -1)   
  11.         perror ("fail to fork"), exit (1);   
  12.     else if (pid == 0)   
  13.     {   
  14.         printf ("这是子进程 pid = %d", getpid ());   
  15.         printf ("父进程的 ppid = %d\n",  getppid ());   
  16.     }   
  17.     else   
  18.     {   
  19.         pr = wait (NULL); 
  20.         //while (1);   
  21.         sleep (10); //可以保证子进程先被调度   
  22.         printf ("这是父进程 ppid = %d\n", getpid ());   
  23.    
  24.     }   
  25.    
  26.     return 0;   
  27. }   
  28.  
  29. 实验: 
  30. 在一个终端执行 ./a.out 
  31. # ./a.out  
  32. 这是子进程 pid = 3951父进程的 ppid = 3950
  33. (十秒后) 
  34. 这是父进程 ppid = 3950 
  35.  
  36. 在另一个终端,查看进程信息 
  37. # ps -C a.out -o pid,ppid,pgid,sid,comm
  38.   PID  PPID  PGID   SID COMMAND
  39. 3950  3868  3950  3868 a.out

                                   
                                   
               
                   
相关标签: UNIX