守护进程
终端
在Unix系统中用户通过终端后得到一个shell进程,这个终端成为shell进程的控制终端,控制终端信息保存在pcb中,而fork会复制pcb中的信息所以由shell进程启动的其他进程(在shell里相当于fork一个子进程 再exec我们要执行的可执行文件 所以其实我们在shell中启动的可执行文件都有一个共同的父进程 就是shell进程)的控制终端也是这个终端。
echo $$可以查看当前终端的pid号 在ps -aux中shell进程 叫-bash
tty 可以查看当前终端设备 /dev/pts/1
默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出也是输出到显示器上。
在控制终端输入一些特殊的控制键可以给前台进程(在当前终端中运行的进程)发信号,例如ctrl c会产生SIGINT信号ctrl \会产生SIGQUIT信号。
后台进程是没有控制终端的
进程组
进程组和会话在进程之间形成了一种两级层次关系:进程组是一组进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持shell作业控制而定义的抽象概念,用户通过shell能够交互式地在前台或后台运行命令。
进程组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建改组的进程,将其进程PID作为该进程组的PGID,子进程会继承其父进程所属的进程组PGID。
进程组的生命周期为,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
会话
会话是一组进程组的集合。会话首进程是创建该新会话的进程,其PID会成为会话ID。子进程会继承其父进程的会话ID。
一个会话中所有进程共享单个控制终端,控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端只能作为一个(不能多)会话的控制终端。
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组(在shell里运行了一个可执行程序后,这个可执行程序就将这个终端占用了 无法再执行其他的shell命令)。只有前台进程组中的进程才能从控制终端中读取读入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。(控制终端只能控制前台进程组 其有个属性就是前端PGID当前前台进程组号)
当控制终端的连接建立起来后,会话首进程会成为该终端的控制进程。
![]() |
|---|
find /2 > /dev/null | wc -l & > /dev/null重定向到这个设备 |管道符创建子进程 wc -l统计文件个数 &在后台运行
sort < longlist | uniq -c 管道符创建子进程| uniq -c
在执行了这两条命令后可得到上图的会话视图 PPID为父进程的PID,SID为会话ID
进程组会话操作函数
pid_t getpgrp(void); 得到当前进程组PGID
pid_t getpgid(pid_t pid); 获取指定进程的进程组PGID
int setpgid(pid_t pid,pid_t pgid); 设置指定进程的进程组
pid_t getsid(pid_t pid); 获取指定进程的会话ID
pid_t setsid(void); 创建新会话 会话id与调用这个函数的进程PID相同
守护进程(Daemon Process) 通常说的Daemon 进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事情。一般采用以d结尾的名字
守护进程具有下来特征:
声明周期很长 守护进程会在系统启动的时候被创建并一直运行直到系统被关闭。
它在后台运行并且不拥有控制终端。没有控制终端是为了确保内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号 (如SIGINT、SIGQUIT) 但是可以手动通过kill -9 pid号 来杀死这个守护进程
Linux 的大多数服务器就是用守护进程实现的 比如Internet服务器inetd,Web服务器httpd等
守护进程的创建步骤
执行fork 之后父进程退出 子进程继续执行 (1执行可执行程序父进程是组首进程 但是会话首进程不能是组首进程(bash除外?如果能的话 组长创建了一个新的会话则组长将会从之前的会话脱离,他的组也将成为一个孤儿进程组) 所以为了之后创建会话需要在子进程中执行 2并且父进程退出后子进程就在后台运行了 控制终端可以执行其他命令 3子进程拥有的PID与当前父进程的组ID会话ID没有冲突,所以可用子进程创建新的会话 进程组ID和会话ID都是唯一的不能重复的)
子进程调用setsid() 开启一个新会话 (开启会话会执行 为子进程创建一个进程组 以子进程pid为这个组号 并且以子进程id创建新会话的sid 这个新会话是没有跟控制终端产生连接的即控制终端无法控制这个子进程(守护进程) 只是没有控制终端但是还是有终端的联系)
清除进程的umask 以确保当前守护进程创建文件和目录时拥有所需的权限
修改进程的当前工作目录 通常修改为/根目录 (如果守护进程是在U盘中启动的 当U盘的守护进程启动后并将目录改为根目录其不再依赖于U盘文件系统并且其虚拟内存空间已经生成 则可以卸载U盘 但是这个守护进程还是在系统中运行的)
关闭守护进程从其父进程继承而来的所有打开着的文件描述符
关闭了0、1、2(标准输入 标准输出 标准错误)文件描述符后,如果一些系统调用用到了这些描述符则会报错,所以守护进程通常会打开/dev/null 并使用dup2()使所有这些描述符指向这个设备(系统调用会向终端输出一些 但是我们不希望后台程序输出信息到屏幕)
其他核心业务逻辑
//实现一个守护进程 每隔两秒获取系统实践并写入到磁盘文件#include <stdio.h>#include <sys/stat.h>#include <sys/types.h>#include <unistd.h>#include <fcntl.h>#include <sys/time.h> //timer#include <time.h> //time#include <signal.h>#include <stdlib.h>#include <string.h>void sig_handler(int num);int write_fd;int main(){//1 创建子进程 退出父进程//会话首进程不能是进程组首进程 但是父进程是当前进程组首进程不满足要求所以不行 但子进程不是pid_t pid = fork();if (pid > 0)return 0; //父进程退出//2 在子进程下创建一个新的会话//新会话是没有控制终端 所以没有控制终端的信号可以杀死这个进程setsid(); //创建新的会话 返回会话的id(会话首进程id即这个子进程的pid)//3 设置掩码 设置用户和组对文件权限的掩码umask(022);//4 更改工作目录chdir("/home/wen");//5 关闭 重定向文件描述符//因为父进程没有打开其他文件 所以重定向fd=0,1,2的标准输入输出错误int fd = open("/dev/null", O_RDWR);dup2(fd, STDIN_FILENO); //重定向到/dev/nulldup2(fd, STDOUT_FILENO); //重定向到/dev/nulldup2(fd, STDERR_FILENO); //重定向到/dev/null//6 核心业务逻辑//每隔两秒获取系统实践并写入到磁盘文件//注册SIGALRM的信号的回调函数write_fd = open("time.txt", O_CREAT | O_RDWR | O_APPEND, 0664); //O_APPEND追加写struct sigaction act;act.sa_flags = 0; //表示使用sa_handler来进行函数回调act.sa_handler = sig_handler;sigaction(SIGALRM, &act, NULL);sigemptyset(&act.sa_mask); //清空临时阻塞信号集表示 不阻塞任何信号struct itimerval val;val.it_interval.tv_sec = 2; //定时器周期时间val.it_interval.tv_usec = 0; //虽然用不到微秒 但还是设置一下 否则是个随机值 负数的话 是tv_sec-|tv_usec| 正数的话是tv_sec+tv_usecval.it_value.tv_sec = 2; //过多长时间第一次启动定时器val.it_value.tv_usec = 0; //setitimer(ITIMER_REAL, &val, NULL);while (1){;//sleep(60); //12秒后守护进程自动关闭 有问题// ITIMER_REAL,定时器递减到0时将发出SIGALRM信号。// 不要小看这个SIGALRM信号,通过阅读说明、源码看出,// linux中的sleep、usleep、select、poll等函数的超时结果均也是使用的SIGALRM// 作为结束信号。若你的定时器按一定周期一直在发SIGALRM信号,// 那么以上几个函数均有极大可能被提前打断,当你的定时器周期设的很短,// 比如10ms时,以上几个函数几乎变得不可使用。//当调用一次alarm信号后sleep直接被打断认为sleep到时间了 直接break//https://blog.csdn.net/spiremoon/article/details/107459814//break;}close(write_fd);return 0;}// sleep() 函数使用的就是实时时钟CLOCK_REALTIMER// 所以使用信号值SIGALRM会中断sleep(int second) 函数的休眠;void sig_handler(int num){//捕捉到alrm信号获取系统时间 写入磁盘文件time_t tm = time(NULL); //1970 1 1到现在的秒数struct tm *loc = localtime(&tm); //将time_t这个秒数转换为年月日时分秒// char buf[1024];// sprintf(buf, "%d-%d-%d %d:%d:%d\n", loc->tm_year, loc->tm_mon, loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec); //年月日 时分秒// printf("%s\n", buf); //num=14 SIGALRMchar *str = asctime(loc);write(write_fd, str, strlen(str));}
终端 控制终端 控制台 tty
终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。
在Linux系统的设备特殊 文件目录 /dev/ 下,终端特殊设备文件一般有以下几种:
1.串行端口终端(/dev/ttySn)
串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间这些串行端口设备通常被称为终端设备,因为那时它的最大用途就是用来连接终端。这些串行端口所对应的设备名称是/dev/tts/0(或/dev/ttyS0),/dev/tts/1(或/dev/ttyS1)等,设备号分别是(4,0),(4,1)等,分别对应于DOS系统下的COM1、COM2等。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。例如,在命令行提示符下键入:echo test > /dev/ttyS1会把单词”test”发送到连接在ttyS1(COM2)端口的设备上
2.伪终端(/dev/pty/)
伪终端(Pseudo Terminal)是成对的逻辑终端设备(即master和slave设备,对master的操作会反映到slave上)。
例如/dev/ptyp3和/dev/ttyp3(或者在设备文件系统中分别是/dev/pty /m3和 /dev/pty/s3)。它们与实际物理设备并不直接相关。如果一个程序把ptyp3(master设备)看作是一个串行端口设备,则它对该端口的读/ 写操作会反映在该逻辑终端设备对应的另一个ttyp3(slave设备)上面。而ttyp3则是另一个程序用于读写操作的逻辑设备。telnet主机A就是通过“伪终端”与主机A的登录程序进行通信。
3.控制终端(/dev/tty)
如果当前进程有控制终端(Controlling Terminal)的话,那么/dev/tty就是当前进程的控制终端的设备特殊文件。可以使用命令”ps –ax”来查看进程与哪个控制终端相连。对于你登录的shell,/dev/tty就是你使用的终端,设备号是(5,0)。使用命令”tty”可以查看它具体对应哪个实际终端设备。/dev/tty有些类似于到实际所使用终端设备的一个联接。
4.控制台(/dev/ttyn, /dev/console)
控制台(console): 显示系统消息的终端就叫控制台,Linux 默认所有虚拟终端都是控制台,都能显示系统消息。
但有时专指CLI下的模拟终端设备的一个程序,和gnome-terminal,urxvt,mlterm,xterm等相同,只是CLI和GUI界面的区别。一般console有6个,tty1-6,CTRL+ALT+fn切换。还没听说过怎么换console
在Linux 系统中,计算机显示器通常被称为控制台终端(Console)。它仿真了类型为Linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之相关联:tty0、tty1、tty2 等。当你在控制台上登录时,使用的是tty1。使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。tty1–tty6等称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上(这时也叫控制台终端)。因此不管当前正在使用哪个虚拟终端,系统信息都会发送到控制台终端上。/dev/console即控制台,是与操作系统交互的设备,系统将一些信息直接输出到控制台上。只有在单用户模式下,才允许用户登录控制台。
虚拟终端:
屏幕和键盘只是一个终端,可能不够用,又不想增加设备投入,就产生了虚拟终端。gnome-terminal,urxvt,mlterm,xterm等等:
是一个程序,职责是模拟终端设备,和虚拟终端的区别表面上在于它以 GUI 形式的窗口出现,内部则是程序结构和系统控制结构有所不同,但本质上差不多。
shell是一个抽象概念,shell的一切操作都在计算机内部,负责处理人机交互,执行脚本等,是操作系统能正常运行的重要组成部分bash,ash,zsh,tcsh等是shell这个抽象概念的一种具体的实现,都是一个程序,都能生成一个进程对象。
如果想换shell的程序,可以修改/etc/passwd,把里面的/bin/bash换成你想要的shell,或者用chsh命令来切换shell与终端的关系:shell把一些信息适当的输送到终端设备,同时还接收来自终端设备的输入。一般每个shell进程都会有一个终端关联,也可以没有。
消息队列 自学
![]() |
|---|
消息队列可以是一种消息链表。有足够的权限的线程可以往队列中放置消息,有足够读权限的线程可以从队列中取走消息。每个消息都是一个记录,它由发送者赋予一个优先级。在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。这根管道和FIFO是相反的,对于后者来说,除非读出者已经存在,否则现有 写入者是没有意义的。
一个进程可以往某个队列中写入一些消息,然后终止,再让另外一个进程在以后某个时刻读出这些消息。我没说过消息队列具有随内核的持续性,这跟管道和FIFO不一样,当一个管道和FIFO的最后一次关闭发生时,仍然在该管或FIFO上的数据将被丢弃。
![]() |
|---|
队列属性
struct msg_msg{struct list_head m_list;long m_type; //消息类型int m_ts; //消息大小struct msg_msgseg *next;//下一个消息位置void *security; //the actual message follows immediately 真正消息位置}
用管道来实现进程间通信的机制是两个进程利用管道文件来实现数据交流
![]() ![]() |
|---|
key_t ftok(const char *path, int id);
消息队列、共享内存和信号量三种进程间通信方式我们称为XSI IPC,当我们调用三种IPC的get函数创造一个IPC结构(比如消息队列)时,会返回相应的IPC标识符,然后我们就能用这个标识符对这个IPC结构进行操作,完成进程间通信,但是这个IPC标识符是这个IPC结构的内部名,我们在一个进程中创建一个IPC结构后另一个进程如何也连接到这个IPC结构呢
struct msqid_ds{struct ipc_perm msg_perm; //权限struct msg *msg_first; //指向消息头struct msg *msg_last; //指向消息尾__kernel_tiem_t msg_stime; //last msgsnd time 最近发送消息时间__kernel_tiem_t msg_rtime; //lsat msgrcv time 最近接受消息时间__kernel_tiem_t msg_ctime; //last change timeunsigned long msg_lcbytes; //Reuse junk fields for 32 bitunsigned long msg_lqbytes; //dittounsigned short msg_qnum; //current number of bytes on queue 当前队列大小unsigned short msg_qbytes; //max number of bytes on queue 队列最大值__kernel_ipc_pid_t msg_lspid; //最近msgsnd 的pid__kernel_ipc_pid_t msg_lrpid; //最近receive 的pid};struct ipc_perm{//消息队列的第一个成员类型__kernel_key_t key;__kernel_uid_t uid;__kernel_gid_t gid;__kernel_uid_t cuid;__kernel_gid_t cgid;__kernel_mode_t mode;unsigned short seq;}
可以观察到,第一个成员就是key——键。
再来看消息队列的get函数原型:
int msgget(key_t key, int msgflg);
这里的第一个参数就是key,这个函数在一个进程中正是根据这里的key值创建相应的消息队列,返回消息队列的标识符,而另一个进程只要得到相同的key值就可以用msgget函数得到想要的消息队列。
而ftok函数正是根据两个参数创建一个“键”并返回,只要两个参数相同,返回的“键”也相同(关于ftok函数创建key的具体过程在《UNIX环境高级编程》的第15章有具体论述)。第一个参数表示路径名,第二个参数表示一个ID,两个参数可以任意给。
msgget函数的第二个参数由9个权限标志构成,其用法与创建文件时使用的mode模式一样,通常我们使用到IPC_CREAT和IPC_EXCL两个权限标志,IPC_CREAT表示如果无则创建返回,有则直接返回;IPC_EXCL表示无则创建返回,有则报错。一般在创建消息队列时将两者合在一起使用,即IPC_CREAT | IPC_EXCL,这样就可以保证得到一个全新的消息队列。
如果要删除消息队列可以用msgctl函数,当然这个函数的功能不止删除,这是一个控制函数,其原型为:int msgctl(int msgid, int cmd, struct msqid_ds buf);
返回值:若成功返回0,失败返回-1。
参数:msgid:由msgget函数返回的消息队列标识符
cmd:将要采取的动作,有三个值,删除消息队列用IPC_RMID。
buf:删除时传入0。
向消息队列中传入“消息”可用msgsnd函数:
int msgsnd(int msgid, const void msgp, size_t msgsz, int msgflg);
返回值:成功返回0,失败返回-1。
参数:msgid:消息队列表示符。
msgp:指向要发送的消息的指针。
msgsz:要发送消息的长度,这个长度不包含保存消息类型的那个长整型(mtype)。
msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情。msgflg=IPC_NOWAIT表示队列满不等待,返回EAGAIN错误。
从消息队列中获取消息可用msgrcv函数:
ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long msgtype, int msgflg);
返回值:成功返回实际放到接收缓冲区里去的字符个数,失败返回-1。
msgid:消息队列id。
msgp:接收消息的缓冲区。
msgsz:要获取的消息长度,不包含消息中的类型长度(mtype)。
msgtype:要接收的消息的类型。
msgflg:控制着队列中没有想要类型的消息可供接收时将要发生的事。
发送的消息的参考结构:
struct msgbuf {
long mtype;//消息的类型
char mtext[1];//消息的长度
}
每个消息的最⼤大⻓长度是有上限的(MSGMAX),每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有一个上限(MSGMNI)。
特性:1、发送带类型数据块。2、是全双工的,可双向通信。3、生命周期随内核,必须显式删除。
用消息队列实现两个进程间对话:
Makefile文件
.PHONY : all
all : server client
server : server.c comm.c
gcc -o server server.c comm.c
client : client.c comm.c
gcc -o client client.c comm.c
.PHONY : clean
clean :
rm -f server client
//comon.h#ifndef _COMM_H_#define _COMM_H_#include <stdio.h>#include <stdlib.h>#include <assert.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>#include <string.h>#include <strings.h>#define PATHNAME "."#define PROJ_ID 0x6666#define SERVER_TYPE 1#define CLIENT_TYPE 2struct mymsg{long mtype;char mtext[1024];};int CreatMsgQueue();int GetMsgQueue();int DelQueue(int msgid);int SendMsg(int msgid, char *buf, long mtype);ssize_t RecMsg(int msgid, char *buf, long mtype);#endif //__COMM_H_
common.c
#include "comm.h"static int _GetCommMsgQueue(int flags){key_t _key = ftok(PATHNAME, PROJ_ID);if(_key < 0){perror("ftok");return -1;}int msgid = msgget(_key, flags);if(msgid < 0){printf("msgget\n");}return msgid;}int CreatMsgQueue(){return _GetCommMsgQueue(IPC_CREAT|IPC_EXCL|0666);}int GetMsgQueue(){return _GetCommMsgQueue(IPC_CREAT);}int DelQueue(int msgid){return msgctl(msgid, IPC_RMID, NULL);}int SendMsg(int msgid, char *buf, long mtype){assert(NULL != buf);struct mymsg msg;msg.mtype = mtype;strcpy(msg.mtext, buf);return msgsnd(msgid, (void*)&msg, sizeof(msg.mtext), 0);}ssize_t RecMsg(int msgid, char *buf, long mtype){assert(NULL != buf);struct mymsg msg;int msglen = msgrcv(msgid, (void*)&msg, sizeof(msg.mtext), mtype, 0);if(msglen < 0){perror("msgrcv");return -1;}strcpy(buf, msg.mtext);return msglen;}
server.c 收发
#include "comm.h"int main(){char buf[1024] = { 0 };int msgid = GetMsgQueue();int msglen = 0;if(msgid < 0)exit(EXIT_FAILURE);while(1){if((msglen = RecMsg(msgid, buf, CLIENT_TYPE)) < 0)break;buf[msglen] = 0;write(1, "client:>", strlen("client:>"));write(1, buf, strlen(buf));if(strcmp(buf, "quit\n") == 0)break;write(1, "server:>", strlen("server:>"));if((msglen = read(0, buf, sizeof(buf))) < 0)break;buf[msglen] = 0;if(SendMsg(msgid, buf, SERVER_TYPE) < 0)break;if(strcmp(buf, "quit\n") == 0)break;}DelQueue(msgid);return 0;}
client.c
#include "comm.h"int main(){char buf[1024] = { 0 };int msgid = CreatMsgQueue();int msglen = 0;if(msgid < 0)exit(EXIT_FAILURE);while(1){write(1, "client:>", strlen("client:>"));if((msglen = read(0, buf, sizeof(buf))) < 0)break;buf[msglen] = 0;if(SendMsg(msgid, buf, CLIENT_TYPE) < 0)break;if(strcmp(buf, "quit\n") == 0)break;if((msglen = RecMsg(msgid, buf, SERVER_TYPE)) < 0)break;buf[msglen] = 0;if(strcasecmp(buf, "quit\n") == 0)break;write(1, "server:>", strlen("server:>"));write(1, buf, strlen(buf));}return 0;}
管道和FIFO用来实现进程间互发非常短小频率很高的消息 两个进程间通信
共享内存用于实现进程间共享非常庞大 读写频率高的消息(配合信号量 不是信号 使用)多个进程间通信 现在更多的是用多线程+锁+线程间共享数据 共享内存效率确实是高 在高频交易系统中有用到
现在一般除非有非常有说服力的理由 一般都用socket 其他情况用socket socket有包装数据和解包数据没有pipe FIFO快但是现在不计较这一点点的速度损失
基于socket的轻量级消息库 ZeroMQ RabbitMQ





