进程(四)
1.终端
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),在讲进程时讲过,控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号号,例如Ctrl-C表示SIGINT,Ctrl-表示SIGQUIT。
登录终端后会启动一个shell
init-->fork-->exec-->getty-->用户输入帐号-->login-->输入密码-->exec-->shell
who
tty表示字符终端,pts表示图形终端(利用了tty7实现了图形终端)
文件与I/O中讲过,每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。每个进程控制块(PCB)都会记录控制终端的信息。下面我们通过实验看一下各种不同的终端所对应的设备文件名。
#include <unistd.h> #include <stdio.h> int main() { printf("fd 0: %s ", ttyname(0)); printf("fd 1: %s ", ttyname(1)); printf("fd 2: %s ", ttyname(2)); return 0; }
硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下Ctrl-Z,对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。(特殊按键都是在内核层面由线路规程解释,转成信号)
终端设备模块
网络终端
虚拟终端或串口终端的数目是有限的,虚拟终端(字符控制终端)一般就是/dev/tty1∼/dev/tty6六个,串口终端的数目也不超过串口的数目。然而网络终端或图形终端窗口的数目却是不受限制的,这是通过伪终端(Pseudo TTY)实现的。一套伪终端由一个主设备(PTYMaster)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程。从设备和上面介绍的/dev/tty1这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件而是访问主设备。网络终端或图形终端窗口的Shell进程以及它启动的其它进程都会认为自己的控制终端是伪终端从设备,例如/dev/pts/0、/dev/pts/1等。下面以telnet为例说明网络登录和使用伪终端的过程。
网络终端
每按一个键都要在网络上走个来回!
2.进程组
一个或多个进程的集合,进程组ID是一个正整数。 用来获得当前进程进程组ID的函数。
pid_t getpgid(pid_t pid) pid_t getpgrp(void)
获得父子进程组ID
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { pid_t pid; if ((pid = fork()) < 0) { perror("fork"); exit(1); } else if (pid == 0) { printf("child process PID is %d ",getpid()); //以下三条是等价的 printf("Group ID is %d ",getpgrp()); printf("Group ID is %d ",getpgid(0)); printf("Group ID is %d ",getpgid(getpid())); exit(0); } sleep(3); printf("parent process PID is %d ",getpid()); printf("Group ID is %d ",getpgrp()); return 0; }
组长进程标识:其进程组ID==其进程ID
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止,只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)
一个进程可以为自己或子进程设置进程组ID
int setpgid(pid_t pid, pid_t pgid) 如改变子进程为新的组,应在fork后,exec前使用 非root进程只能改变自己创建的子进程,或有权限操作的进程
setpgid()加入一个现有的进程组或创建一个新进程组,如改变父子进程为新的组
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { pid_t pid; if ((pid = fork()) < 0) { perror("fork"); exit(1); } else if (pid == 0) { printf("child process PID is %d ",getpid()); printf("Group ID of child is %d ",getpgid(0)); // 返回组id sleep(5); printf("Group ID of child is changed to %d ",getpgid(0)); exit(0); } sleep(1); setpgid(pid,pid); // 父进程改变子进程的组id为子进程本身 sleep(5); printf("parent process PID is %d ",getpid()); printf("parent of parent process PID is %d ",getppid()); printf("Group ID of parent is %d ",getpgid(0)); setpgid(getpid(),getppid()); // 改变父进程的组id为父进程的父进程 printf("Group ID of parent is changed to %d ",getpgid(0)); return 0; }
3.会话(一个终端里的所有进程都所有同一个会话,在终端退出时,默认所有的进程退出)
cat |cat|cat &
ps ajx
关闭终端另开一个新的
观察 ps ajx
pid_t setsid(void)
1.调用进程不能是进程组组长,该进程变成新会话首进程(session header) 2.该进程成为一个新进程组的组长进程。 3.需有root权限(ubuntu不需要) 4.新会话丢弃原有的控制终端,该会话没有控制终端 5.该调用进程是组长进程,则出错返回 6.建立新会话时,先调用fork, 父进程终止,子进程调用。
pid_t getsid(pid_t pid)
pid为0表示察看当前进程session ID
ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
应用:服务器程序和一些控制程序都是脱离终端的,大多都是通过使其成为一个新的会话实现的。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { pid_t pid; if ((pid = fork())<0) { perror("fork"); exit(1); } else if (pid == 0) { printf("child process PID is %d ", getpid()); printf("Group ID of child is %d ", getpgid(0)); printf("Session ID of child is %d ", getsid(0)); sleep(10); setsid(); // 子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话进程 printf("Changed: "); printf("child process PID is %d ", getpid()); printf("Group ID of child is %d ", getpgid(0)); printf("Session ID of child is %d ", getsid(0)); sleep(20); exit(0); } return 0; } 备注:写完后,关闭,另开一个终端,ps看子进程是否还存在
4.守护进程
概念
Daemon(精灵)进程,是Linux中的后台服务进程,生存期较长的进程(一般都是从电脑开机到电脑关机),通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
模型
守护进程编程步骤
1. 创建子进程,父进程退出 所有工作在子进程中进行 形式上脱离了控制终端 2. 在子进程中创建新会话 setsid()函数 使子进程完全独立出来,脱离控制 3. 改变当前目录为根目录 chdir()函数 防止占用可卸载的文件系统 也可以换成其它路径 4. 重设文件权限掩码 umask()函数 防止继承的文件创建屏蔽字拒绝某些权限 增加守护进程灵活性 5. 关闭文件描述符 继承的打开文件不会用到,浪费系统资源,无法卸载 6. 开始执行守护进程核心工作 7. 守护进程退出处理
代码模型
#include <stdlib.h> #include <stdio.h> #include <fcntl.h> void daemonize(void) { pid_t pid; /* * 成为一个新会话的首进程,失去控制终端 */ if ((pid = fork()) < 0) { perror("fork"); exit(1); } else if (pid != 0) /* parent */ exit(0); setsid();
/* * 改变当前工作目录到/目录下. */ if (chdir("/") < 0) { perror("chdir"); exit(1); } /* 设置umask为0 */ umask(0); /* * 重定向0,1,2文件描述符到 /dev/null,因为已经失去控制终端,再操作0,1,2没有意义. */ close(0); open("/dev/null", O_RDWR); dup2(0, 1); dup2(0, 2); } int main(void) { daemonize(); while(1); /* 在此}循环中可以实现守护进程的核心工作 */ }
运行这个程序,它变成一个守护进程,不再和当前终端关联。用ps命令看不到,必须运行带x参数的ps命令才能看到。另外还可以看到,用户关闭终端窗口或注销也不会影响守护进程的运行。
思考:守护进程为什么要和控制终端脱离?
若没有脱离,关闭控制终端,守护进程就被关闭,达不到效果。