进程组
进程组就是一个或多个进程的集合,进程组ID是一个正整数,每个进程组都有一个组长进程,组长进程的进程号等
于进程组ID。组长进程可以创建一个进程组、创建该组中的进程。
**进程组生存期:**进程组创建,到最后一个进程离开进程组(终止或转移到另一个进程组)。
一个进程可以为自己或为其子进程设置进程组ID。
进程组可被分为一个前台进程组和一个或多个后台进程组。为什么要这么分呢?前台进程组是指需要与终端进行交
互的进程组(只能有一个)。
比如有些进程是需要完成IO操作的,那么这个进程就会被设置为前台进程组.当我们键入终端的中断键和退出键时,就会将信号发送到前台进程组中的所有进程。而后台进程组是指不需要与终端进程交互的进程组,比如:一些进程不需要完成IO 操作,或者一些守护进程就会被设置为后台进程组。
再比如,可以直接向一个进程组发送信号,例如:
pkill_test.c源码:
#include
#include
int main(int argc, char** argv)
{
int pid = fork();
if(pid == 0)
{
while(1)
sleep(1);
}
else if(pid > 0)
{
while(1)
sleep(1);
}
return 0;
}
运行pkill_test,pkill_test通过fork创建了一个子进程,父子进程在同一个组内,这时可以通过pkill -g把这一个小组的进程全部杀死。
进程组标识PGID(process group ID):用于进程所属的进程组ID。
#include
#include
#include
int main(int argc, char** argv)
{
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;
}
设置进程组
使用 setpgid() 加入一个现有的进程组或创建一个新进程组
setpgid将参数pid指定进程所属的进程组ID设为参数pgid指定的进程组ID。
如果参数pid 为0,则会用来设置当前进程的进程组ID。
如果参数pgid为0,则由pid指定的进程ID将用作进程组ID。
一个进程只能为它自己或它的子进程设置进程组ID。
如改变子进程为新的组,应在fork后,exec前使用。
源码:setpgid_test.c
#include
#include
#include
int main(int argc, char** argv)
{
pid_t pid;
if ((pid = fork()) < 0)
{
perror("fork");
exit(-1);
}
else if (pid == 0)
{
printf("child process PID [%d]\n",getpid());
printf("child group ID [%d]\n",getpgid(0)); // 返回组id
sleep(5);
printf("child group ID [%d]\n",getpgid(0));
exit(0);
}
sleep(1);
setpgid(pid,pid); // 父进程改变子进程的组id为子进程本身
sleep(5);
printf("parent process PID [%d]\n", getpid());
printf("parent's parent process PID is [%d]\n", getppid());
printf("parent group ID [%d]\n",getpgid(0));
setpgid(getpid(),getppid()); // 改变父进程的组id为父进程的父进程
printf("parent group ID [%d]\n",getpgid(0));
return 0;
}
终端
linux 的终端就是控制台, 是用户与内核交互的平台, 通过输入指令来控制内核完成任务操作。外形是一个方框,有光标在闪烁。
在Linux系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),控制终端是保存在task_struct中的信息,而我们知道fork会复制task_struct中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。
默认情况下,每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
ttyname函数可以由文件描述符查出当前进程对应的终端设备文件名,该文件描述符必须指向一个终端设备而不能是任意文件。下面我们通过实验看一下各种不同的终端所对应的设备文件名。
#include
#include
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;
}
登录后的shell其输入输出是连接到用户使用的终端的,不管是本地登录的tty,还是远程登录的pty。但是,为什么要有终端呢?shell的输入直接接到键盘、输出直接接到显示器,这样不行么?尤其是远程的情况,shell的输入输出为什么不能直接接到网络,而非要弄一个pty出来呢? 最容易想到的一点,终端能够使得上层不必关心输入输出设备本身的细节,只管对其读写就行了。不过这一点似乎并不是终端所特有的,因为VFS已经能够胜任了。应用程序open设备文件,得到fd,然后同样只用管对其读写就行了,而不用关心这个fd代表的是键盘、还是普通文件,具体的细节已经被隐藏在设备驱动程序之中。
不过,相比于普通的读写,终端还实现了很多对输入输出的处理逻辑。如:
1、回车换行的转换:定义输入输出如何映射回车换行符。比如:回车键是\r、还是\n、还是\r\n;再如:\n应该如何打印到屏幕上,是回车+换行、还是只换行不回车、等等。
2、行编辑:允许让输入字符不是立马送到应用程序,而是在换行以后才能被读取到。未换行的输入字符可以通过退格键进行编辑(比如在你密码输错的时候,是可以用CTRL+退格来进行编辑的)。
3、回显:可以让输入字符自动被回显到终端的输出上。于是,键盘每输入一个字符都能在显示器上看到它,而这些字符其实很可能是还没被应用程序读取到的(因为有行编辑)。
4、功能键:允许定义功能键。比如最常用的Ctrl+C,杀死前台进程,就是由终端来触发的。终端检测到Ctrl+C输入,会向前台进程组发送SIGINT信号。
5、输入输出流向控制,只有前台进程组能够从终端中读数据、而前台后台程序都能向终端写数据。这点也是必须的,跟用户进程交互的是前台进程,用户的输入当然不能被其他后台进程抢走。但是一个进程是前台还是后台,是它自己所不知道的,没法靠进程自己来判断什么时候可以读终端、什么时候不能读。所以需要终端来提供支持,如果后台进程读这个终端,终端的驱动程序将向其发送SIGTTIN信号,从而将其挂起。直到shell将其重新置为前台进程时(通过fg命令),该进程才会继续执行;可以说,终端是人机交互时,应用程序与用户之间的一个中间层。如果应用程序是在跟人交互,使用终端是其不二的选择;否则则没有必要使用终端。
会话
会话是基于连接的。会话的源头,就是用户与系统之间连接的启用。
用户登录是一个会话的开始。登录之后,用户会得到一个跟用户使用的终端相连的进程,这个进程被称作是这个会话的leader,会话的id就等于该进程的pid。由该进程fork出来的子进程都是这个会话的成员。leader进程的退出,将导致它所连接的终端被hangup,这意味着会话结束。并向所有的会话中的进程发射信号。SIGHUP挂起信号。默认信号处理的进程退出。
一个会话(session)只能有一个终端,称为controlling terminal,此外,建立或者改变终端与会话的联系只能由会话领导者(session leader)来进行。会话可以有终端也可以没有终端。
一个会话又可以包含多个进程组。一个会话对应一个控制终端。
查看会话ID
pid为0表示察看当前进程session ID , ps -ajx 命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
组长进程不能成为新会话leader进程,新会话leader进程必定会成为组长进程。
想要满足以上条件,建立新会话时,先调用fork, 父进程终止,子进程调用setsid。
#include
#include
#include
int main(void)
{
pid_t pid;
if ((pid = fork())<0)
{
perror("fork");
exit(-1);
}
else if (pid == 0)
{
printf("child process PID [%d]\n", getpid());
printf("child group ID [%d]\n", getpgid(0));
printf("child session ID [%d]\n", getsid(0));
sleep(5);
setsid(); // 子进程非组长进程,故其成为新会话leader进程,且成为组长进程。该进程组id即为会话进程
printf("changed:\n");
printf("child process PID [%d]\n", getpid());
printf("child group ID [%d]\n", getpgid(0));
printf("child session ID [%d]\n", getsid(0));
while(1)
{
sleep(1);
}
}
return 0;
}
守护进程
概念
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。
比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程lpd等。(这里的结尾字母d就是Daemon的意思)。
如何创建守护进程
守护进程编程步骤
释放内存申请,占用的系统资源的释放。
源码:定时写log的守护进程log_test.c
#include
#include
#include
#include
#include
#include
#include
#include
void init_daemon()
{
int pid;
int i;
pid=fork();
if(pid < 0)
{
perror("fork");
exit(-1); //创建错误,退出
}
else if(pid>0) //父进程退出
{
exit(0);
}
/* 子进程创建新的会话 */
setsid();
/* 改变当前工作目录到/目录下.*/
if (chdir("/") < 0)
{
perror("chdir");
exit(1);
}
/* 设置umask为0 */
umask(0);
/*关闭进程打开的文件句柄*/
for(i=0;i<NOFILE;i++)
{
close(i);
}
return;
}
int main(int argc, char** argv)
{
FILE *fp;
time_t t;
init_daemon();
while(1)
{
sleep(60); //等待一分钟再写入
fp=fopen("/home/where/test.log","a+");
if(fp>=0)
{
time(&t);
fprintf(fp,"current time is:%s\n",ctime(&t)); //转换为本地时间输出
fclose(fp);
}
}
return 0;
}