进程控制
#
# 问题
# 父子进程之间在fork后。有哪些相同,那些相异之处呢?
刚fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...
父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
练习:编写程序测试,父子进程是否共享全局变。
提示
重点注意!躲避父子进程共享全局变量的知识误区! 【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解) 特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
# 守护进程
# 终端
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。 Alt + Ctrl + F1、F2、F3、F4、F5、F6 字符终端 pts (pseudo terminal slave) 指伪终端。 Alt + F7 图形终端 SSH、Telnet... 网络终端
# 终端的启动流程:
文件与I/O中讲过,每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。 简单来说,一个Linux系统启动,大致经历如下的步骤: init --> fork --> exec --> getty --> 用户输入帐号 --> login --> 输入密码 --> exec --> bash 硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下Ctrl-z,对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。
# ttyname函数
由文件描述符查出对应的文件名 char *ttyname(int fd); 成功:终端名;失败:NULL,设置errno 下面我们借助ttyname函数,通过实验看一下各种不同的终端所对应的设备文件名。
#include <unistd.h>
#include <stdio.h>
int main(void)
{
printf("fd 0: %s\n", ttyname(0));
printf("fd 1: %s\n", ttyname(1));
printf("fd 2: %s\n", ttyname(2));
return 0;
}
# 进程组
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID==第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID==其进程ID 组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。 进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。 一个进程可以为自己或子进程设置进程组ID
# 会话 - 多个进程组
# 创建会话
创建一个会话需要注意以下6点注意事项:
- 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
- 该进程成为一个新进程组的组长进程。
- 需有root权限(ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 该调用进程是组长进程,则出错返回
- 建立新会话时,先调用fork, 父进程终止,子进程调用setsid
# getsid函数
//获取进程所属的会话ID
//成功:返回调用进程的会话ID;失败:-1,设置errno
pid_t getsid(pid_t pid);
// pid为0表示察看当前进程session ID
ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。 组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
# setsid函数
//创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。
//成功:返回调用进程的会话ID;失败:-1,设置errno
pid_t setsid(void);
//调用了setsid函数的进程,既是新的会长,也是新的组长。
#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\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
sleep(10);
setsid(); //子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话进程
printf("Changed:\n");
printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
sleep(20);
exit(0);
}
return 0;
}
# 创建守护进程模型
- 创建子进程,父进程退出 所有工作在子进程中进行形式上脱离了控制终端
- 在子进程中创建新会话 setsid()函数 使子进程完全独立出来,脱离控制
- 改变当前目录为根目录 chdir()函数 防止占用可卸载的文件系统 也可以换成其它路径
- 重设文件权限掩码 umask()函数 防止继承的文件创建屏蔽字拒绝某些权限 增加守护进程灵活性
- 关闭文件描述符 继承的打开文件不会用到,浪费系统资源,无法卸载
- 开始执行守护进程核心工作
- 守护进程退出处理程序模型
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.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); /* 在此循环中可以实现守护进程的核心工作 */
}