Linux系统编程06 --进程间关系

六、进程间关系

终端

​ 在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),在讲进程时讲过,控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。

​ 终端,在前面的理解终端就是shell,这并不准确,在以前,终端表示的是设备,如打字机,所以终端的英文简写为tty(teletype,电传打字机),在设备文件夹/dev下有一个文件为tty,即代表的是终端设备文件。那么shell是什么呢?shell本身是命令解释器的统称,一般Linux默认的shell是bash,以前将shell称为终端,真正的终端并非是shell,shell只是终端和内核之间翻译命令的进程。

​ 在Unix系统开发时,为了实现多用户系统,就必须要有多个终端(用户与计算机沟通的硬件设备),然而当时电脑非常昂贵,Unix创作者就选择电传打字机这种即有键盘(输入设备),又有打字设备(好比显示器,输出设备),关键是便宜的硬件设备作为终端,通过串口实现多终端多用户访问Unix操作系统下的计算机,所以终端就是早期的tty(电传打字机)。到现在,电传打字机早已经躺在博物馆里面了,现代操作系统指的终端是终端模拟器
在这里插入图片描述
对于 Shell,终端模拟器会「假装」成一个传统终端设备;而对于现代的图形接口,终端模拟器会「假装」成一个 GUI 程序。一个终端模拟器的标准工作流程是这样的:

1.捕获你的键盘输入;
2.将输入发送给 Shell(Shell 会认为这是从一个真正的终端设备输入的);
3.拿到命 Shell 的输出结果;
4.调用图形接口(比如 X11),将输出结果渲染至显示器。

终端模拟器有很多,这里就举几个经典的例子:

  • GNU/Linux:gnome-terminal、Konsole;
  • macOS:Terminal.app、iTerm2;
  • Windows:Win32 控制台、ConEmu 等。
    在这里插入图片描述

​ 部分终端模拟器都是在图形用户界面 (GUI) 中运行的,但是也有例外。

​ 比如在 GNU/Linux 操作系统中,按下 Ctrl + Alt + F1,F2…F6 等组合键可以切换出一个黑不溜秋的全屏终端界面,不过不要被它们唬着了,虽然它们并不运行在图形界面中,但其实它们也是终端模拟器的一种。
​ 这些全屏的终端界面与那些运行在 GUI 下的终端模拟器的唯一区别就是它们是 由操作系统内核直接提供的。这些由内核直接提供的终端界面被叫做 虚拟控制台 (Virtual Console),而上面提到的那些运行在图形界面上的终端模拟器则被叫做 终端窗口 (Terminal Window)。除此之外并没有什么差别。

​ 当然了,因为终端窗口是跑在图形界面上的,所以如果图形界面宕掉了那它们也就跟着完蛋了。这时候你至少还可以切换到 Virtual Console 去救火,因为它们由内核直接提供,只要系统本身不出问题一般都可用。

shell 是一个程序,它接受从键盘输入的命令,然后把命令传递给操作系统去执行。

Linux登录终端和shell过程:

init-->fork-->exec-->getty-->用户输入帐号-->login-->输入密码-->exec-->shell

​ getty到输入密码这个过程是登录终端的过程,getty是向终端模拟器申请开启一个新的tty(这里是第一次开启该终端需要登录,如果是打开前面已经开启过的终端,就不需要登录过程,好比第二次按Ctrl + Alt + F1,第二次打开F1终端就直接进入shell),再登录,登录成功后该新终端创建成功,自动加载shell进程。

  • 在图形界面系统下,首先进入系统,锁屏界面登录用户,在图形桌面下点开terminal(窗口终端),这时会弹出一个窗口,里面自动打开的就是当前用户登录的shell,因为前面登录了系统,所以省略了登录终端的过程。图形界面下打开终端,会省略登录过程,直接将本操作系统的用户登录进去了。
  • 如果使用Ctrl + Alt + F1-F6打开新终端,这打开的并不是窗口终端,而是操作系统直接提供的终端(字符控制终端),需要登录在开启shell。

who命令查看当前系统用户及启动的终端

jiaojian@KyLin:~$ who
jiaojian tty7         2022-03-16 15:39 (:0)
jiaojian tty3         2022-03-16 15:40
jiaojian pts/1        2022-03-16 16:21 (172.20.117.1)

如上,终端有tty7和tty3和pts/1,其中tty7为该系统默认的图像界面终端,tty3为自己开启的终端,pts/s为远程登录开启的虚拟终端。

tty

​ 文件与I/O中讲过,每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。下面我们通过实验看一下各种不同的终端所对应的设备文件名。

打印当前终端名
#include <unistd.h>
#include <stdio.h>
int main()
{
	printf("fd 0: %s\n", ttyname(0));
	printf("fd 1: %s\n", ttyname(1));
	printf("fd 2: %s\n", ttyname(2));
	return 0;
}
0,1,2为终端文件描述符,不能用普通文件描述符。

​ 在程序中出现的文件“/dev/tty”,其含义就是当前终端,文件描述符,0,1,2指向的文件就是“/dev/tty”,而“/dev/tty”是一个类似于泛型指针的文件,其指向的是当前用户使用的终端。所以在不同终端内使用该文件,其指向的就是本终端,如在终端1上运行进程A,A内的“/dev/tty”指向的就是终端1(进程运行的终端),进程的PCB内记录了自己的控制终端。

是否每一个进程都需要一个终端呢?

​ 不是,在系统内有大量的进程的tty为?,即很多进程是没有控制终端的,命令ps -ajx可以查看进程以及其控制终端。

​ 硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下Ctrl-Z,对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。
在这里插入图片描述

网络终端

​ 虚拟终端或串口终端的数目是有限的,虚拟终端(字符控制终端)一般就是/dev/tty1/dev/tty6六个,串口终端的数目也不超过串口的数目。然而网络终端或图形终端窗口的数目却是不受限制的,这是通过伪终端(Pseudo TTY)实现的。一套伪终端由一个主设备(PTYMaster)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程。从设备和上面介绍的/dev/tty1这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件而是访问主设备。网络终端或图形终端窗口的Shell进程以及它启动的其它进程都会认为自己的控制终端是伪终端从设备,例如/dev/pts/0、/dev/pts/1等。下面以telnet为例说明网络登录和使用伪终端的过程。
在这里插入图片描述
如上图为远程登录终端开启以及输入命令操作bash的过程。

​ 如果telnet客户端和服务器之间的网络延迟较大,我们会观察到按下一个键之后要过几秒钟才能回显到屏幕上。这说明我们每按一个键telnet客户端都会立刻把该字符发送给服务器,然后这个字符经过伪终端主设备和从设备之后被Shell进程读取,同时回显到伪终端从设备,回显的字符再经过伪终端主设备、telnetd服务器和网络发回给telnet客户端,显示给用户看。也许你会觉得吃惊,但真的是这样:每按一个键都要在网络上走个来回!

进程组

一个或多个进程的集合,进程组ID是一个正整数。 用来获得当前进程程组ID的函数:F

pid_t getpgid(pid_t pid)
    
[参数]
pid为指定进程pid,查询指定进程的组pgid,如果pid为0,查询当前进程的组pgid
    
pid_t getpgrp(void)
查询当前进程组的pgid

[返回值]
成功返回获取的组pgid,失败返回-1
#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 is %d\n",getpgrp());
		printf("Group ID is %d\n",getpgid(0));
		printf("Group ID is %d\n",getpgid(getpid()));
		exit(0);
	}
	sleep(3);
	printf("parent process PID is %d\n",getpid());
	printf("Group ID is %d\n",getpgrp());
	return 0;
}

组长进程标识:其进程组ID==其进程ID,即pid与pgid相同的进程为组长进程

组长进程可以创建一个进程组,创建该进程组中的进程,然后终止,只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关,并且组长进程终止,原来的gid也不会改变,与组长进程消亡没有关系。

进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组),该进程组就消亡了。

一个进程可以为自己或子进程设置进程组ID

int setpgid(pid_t pid, pid_t pgid)
[参数]
    pid为需要操作的进程pid。
    pgid为要加入的目标进程组pgid。
    如果pid=1000,pgid=1000,相当于给进程1000创建了一个进程组,并且自己是组长
    如果pid=1000,pgid=2000,那么相当于将进程1000加到pgid为2000的进程组内
[返回值]
    成功返回0;失败返回-1。
如改变子进程为新的组,应在fork后,exec前使用
非root进程只能改变自己创建的子进程,或有权限操作的进程

会话

会话是一个或多个进程组的集合,以用户登录系统为例,可能存在如图所示的情况。
在这里插入图片描述

setsid()创建新会话函数
pid_t setsid(void)
[返回值]
    成功创建新会话返回新会话sid;
    失败返回-1

setsid()函数使用条件与特点:

1.调用进程不能是进程组组长,该进程变成新会话首进程(session header) ,体现了进程组与会话的严格层次关系,进程组是包含在会话内的,是会话的组成单位。

2.该进程成为一个新进程组的组长进程进程组pgid为调用进程pid,且该进程变为新会话首进程,新会话sid也为该进程pid。

3.需有root权限才能执行setsid()创建新会话(ubuntu不需要)

4.新会话丢弃原有的控制终端,新会话没有控制终端

5.如果setsid()的调用进程是组长进程,则出错返回-1

6.建立新会话时,先调用fork, 父进程终止,子进程可调用setsid()创建新会话

如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话的严格的层次关系了。

创建新的会话最大的作用是脱离控制终端

组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。

getsid()获取会话sid函数
pid_t getsid(pid_t pid)
[参数]
    pid为需要查询其所属会话的进程pid
    如果pid = 0表示获取当前进程的sid
[返回值]
    成功返回获取的会话sid
    失败返回-1
    

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\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;
}

守护进程

Daemon(精灵)进程,是Linux中的后台服务进程,生存期较长的进程,通常独立于控制终端(即没有控制终端)并且周期性地执行某种任务或等待处理某些事件发生。

守护进程创建步骤
1. 创建子进程,父进程退出
	所有工作在子进程中进行
  形式上脱离了控制终端
2. 在子进程中创建新会话
  setsid()函数
  使子进程完全独立出来,脱离控制
3. 改变当前目录为根(/)目录
  chdir()函数
  防止占用可卸载的文件系统(U盘)
  也可以换成其它路径
4. 重设文件权限掩码
  umask()函数
  防止继承的文件创建屏蔽字拒绝某些权限
  增加守护进程灵活性
5. 关闭文件描述符(0,1,2文件描述符原本是指向终端的,现在脱离终端了,就回收0,1,2)
  继承的打开文件不会用到,浪费系统资源,无法卸载
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);//上一步关闭了0描述符,那么这一步打开null文件,则将描述符0分配给null(先打开文件的文件描述符为该进程文件描述符表中未使用的最小的那个,这里是0)
	dup2(0, 1);
	dup2(0, 2);
}

int main(void)
{
	daemonize();//创建守护进程的函数(自定义)
	while(1); /* 在此循环中可以实现守护进程的核心工作,while里面就是守护进程具体要干是事情了。*/
}

运行这个程序,它变成一个守护进程,不再和当前终端关联。用ps命令看不到,必须运行带x参数的ps命令才能看到。另外还可以看到,用户关闭终端窗口或注销也不会影响守护进程的运行。

如下为创建一个守护进程,该进程每隔5s在/tmp/dameon.log中写入当前时间

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
void mydaemon(void)
{
    pid_t pid_f, i;
    if((pid_f = fork()) < 0)
    {
        perror("foek():");
        exit(1);
    }
    if(pid_f > 0)
    {
        printf("father_pid = %d,and child_pid = %d\n", getpid(), pid_f);
        exit(1);
    }
    pid_t sid_d;
    printf("daemon_pid = %d\n", getpid());
    sid_d = setsid();
    if(chdir("/") < 0)
    {
        perror("chdir:");
        exit(1);
    }
    printf("new sid = %d\n",sid_d);
    umask(0);
    close(0);
    open("/dev/null",O_RDWR);
    dup2(0,1);
    dup2(0,2);
}

int do_daemon(int fd)
{
    time_t t;
    char *time_1;
    char arr[30] = {0};
    time(&t);
    time_1 = strcat(ctime(&t),"\n");
    strcpy(arr, time_1);
    if(write(fd, arr, 25) < 0)
    {
        perror("write:");
    }
    return 0;

}

int main()
{
    mydaemon();
    int fd;
    fd = open("/tmp/daemon.log", O_RDWR | O_CREAT | O_APPEND, 0777);
    while(1)
    {
        do_daemon(fd);
        sleep(5);
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值