您的位置:首页 > 运维架构 > Linux

Linux-进程、进程组、作业、会话、控制终端详解

2017-10-27 15:28 597 查看
From:http://www.cnblogs.com/JohnABC/p/4079669.html

Linux进程优先级的处理--Linux进程的管理与调度(二十二):http://blog.csdn.net/gatieme/article/details/51719208

进程 、进程组、会话、控制终端之间的关系:http://blog.csdn.net/yh1548503342/article/details/41891047

Linux进程控制:http://www.cnblogs.com/cpsmile/p/4382106.html

详细可以查看 APUE 第八章 进程控制 和 第九章 进程关系

一、进程

  传统上,Unix操作系统下运行的应用程序、 服务器以及其他程序都被称为进程,而Linux也继承了来自unix进程的概念。必须要理解下,程序是指的存储在存储设备上(如磁盘)包含了可执行机器指 令(二进制代码)和数据的静态实体;而进程可以认为是已经被OS从磁盘加载到内存上的、动态的、可运行的指令与数据的集合,是在运行的动态实体。这里指的 指令和数据的集合可以理解为Linux上ELF文件格式中的.text .data数据段。

二、进程组

  每个进程除了有一个进程ID之外,还属于一个进程组,那什么是进程组呢?

  顾名思义,进程组就是一个或多个进程的集合。这些进程并不是孤立的,他们彼此之间或者存在父子、兄弟关系,或者在功能上有相近的联系。每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构。同一进程组中的各进程接收来自同一终端的各种信号,每个进程组有一个唯一的进程组ID。每个进程组有一个组长进程,该组长进程的ID等于进程组ID。从进程组创建开始到最后一个进程离开为止的时间称为进程组的生命周期。

  那为啥Linux里要有进程组呢?其实,提供进程组就是为了方便对进程进行管理。假设要完成一个任务,需要同时并发100个进程。当用户处于某种原因要终止 这个任务时,要是没有进程组,就需要手动的一个个去杀死这100个进程,并且必须要严格按照进程间父子兄弟关系顺序,否则会扰乱进程树。有了进程组,就可以将这100个进程设置为一个进程组,它们共有1个组号(pgrp),并且有选取一个进程作为组长(通常是“辈分”最高的那个,通常该进程的ID也就作为进程组的ID)。现在就可以通过杀死整个进程组,来关闭这100个进程,并且是严格有序的。组长进程可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。

   进程必定属于一个进程组,也只能属于一个进程组。 一个进程组中可以包含多个进程。 进程组的生命周期从被创建开始,到其内所有进程终止或离开该组。

  内核中,sys_getpgrp()系统调用用来获取当前进程所在进程组号;sys_setpgid(int pid, int pgid)调用用来设置置顶进程pid的进程组号为pgid。
#include <unistd.h>

pid_t getpgrp(void);    // 返回值:调用进程的进程组ID

int setpgid(pid_t pid, pid_t pgid);    // 返回值:成功,返回0;失败,返回-1

说明:
setpgid用于添加进程到一个现有的进程组,或者创建一个新的进程组。函数将进程ID为pid的进程加入ID为pgid的进程组中。

如果pid == pgid,则pid指定的进程变为进程组长;
如果pid == 0,则使用调用者的进程ID;
如果pgid == 0,则将pid用作进程组ID。


三、作业

  Shell分前后台来控制的,不是进程,而是作业(Job)或者进程组(Process
Group)
。一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,Shell可以运行一个前台作业任意多个后台作业,这称为作业控制。

  作业与进程组的区别:如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。

        作业控制允许在一个终端上启动多个作业(进程组),哪一个作业可以访问该终端以及哪些作业在后台运行。从shell使用作业控制的角度看,用户可以在前台或者后台启动一个作业,例如:vi main.c在前台启动只有一个进程组成的作业,而make all &在后台启动只有一个进程组成的作业

我们可以键入3个特殊字符使得终端程序产生信号,并将它们发送到前台进程组:中断字符(Ctrl + C)产生SIGINT信号;退出字符(Ctrl + \)产生SIGQUIT信号;挂起字符(Ctrl + Z)产生SIGTSTP信号;

只有前台作业才可以接收终端上输入的字符,如果后台作业试图都终端,那么终端驱动程序向后台作业发送特定信号SIGTTIN,该信号将停止此后台作业,而shell则向用户发送通知,然后用户就可以利用shell命令fg将此作业转为前台作业运行。但是如果后台作业输出到控制终端又将发生什么呢?我们可以通过stty命令禁止这种情况。此时,终端驱动程序向后台作业发送SIGTTOU信号,使其进程阻塞,当然此时我们也可以利用fg将其移到前台运行。

四、会话

  再看下会话。由于Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话 。会话是一个或者多个进程组的集合。进程组通常是由shell管道编制在一起的。一个会话可包含多个进程组,但只能有一个前台进程组。每个会话都有一个会话首领(leader),即创建会话的进程。 sys_setsid()调用能创建一个会话。必须注意的是,只有当前进程不是进程组的组长时,才能创建一个新的会话。调用setsid
之后,该进程成为新会话的leader。

  一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。 

  一次登录形成一个会话

  一个会话可包含多个进程组,但只能有一个前台进程组

会话是一个或者多个进程组的集合。进程组通常是由shell管道编制在一起的,如下:



进程调用setsid函数创建一个新的会话:
#include <unistd.h>

pid_t setsid(void);    // 返回值:成功,返回进程组ID;失败,返回-1

说明:
如果调用此函的进程不是一个进程组长,则此函数创建一个新的会话,具体如下:
(1)   该进程变为新会话的会话首进程。
(2)   该进程成为一个新进程组的组成进程,新进程组ID是该进程ID。
(3)   该进程没有控制终端。

pid_t getsid(pid_t pid);    // 返回值:成功,返回会话首进程的进程组ID;失败,返回-1

说明:getsid(0)返回调用进程的会话首进程的进程组ID,如果pid不属于调用者所在的会话,则不返回。


五、控制终端

  会话的领头进程打开一个终端之后, 该终端就成为该会话的控制终端 (SVR4/Linux)  

  与控制终端建立连接的会话领头进程称为控制进程 (session leader) 

  一个会话只能有一个控制终端 

  产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程 

  终端上的连接断开时 (比如网络断开或 Modem 断开), 挂起信号将发送到控制进程(session leader)

 

  进程属于一个进程组,进程组属于一个会话,会话可能有也可能没有控制终端。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。类似地,每个会话也对应有一个领头进程。

  同一会话中的进程通过该会话的领头进程和一个终端相连,该终端作为这个会话的控制终端。一个会话只能有一个控制终端,而一个控制终端只能控制一个会话。用户通过控制终端,可以向该控制终端所控制的会话中的进程发送键盘信号。

 

   同一会话中只能有一个前台进程组,属于前台进程组的进程可从控制终端获得输入,而其他进程均是后台进程,可能分属于不同的后台进程组。

   当我们打开多个终端窗口时,实际上就创建了多个终端会话。每个会话都会有自己的前台工作和后台工作。
一个会话可以有一个控制终端,通常是终端设备或伪终端设备。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的进程组可被分为一个前台进程组和一个后台进程组。需要有一种方法通知内核哪一个进程组是前台进程组,这样便于终端设备驱动程序知道将终端输入和终端产生的信号发送到何处:

#include <unistd.h>

pid_t tcgetpgrp(int fd);    // 返回值:成功,返回前台进程组ID;失败,返回-1

int tcsetpgrp(int fd, pid_pgrpid);    // 返回值:成功,返回0;失败,返回-1

说明:其中,fd为相关联的打开终端。大多数应用程序并不直接调用这两个函数,而是由作业控制shell调用。
需要管理控制终端的应用程序可以调用tcgetsid函数获得控制终端的会话首进程的进程组ID:

#include <termios.h>
pid_t tcgetsid(int fd);    // 返回值:成功,返回会话首进程的进程组ID;失败,返回-1


六、守护进程

守护进程(daemon)常常在系统引导装入时启动,在系统关闭时终止。由于守护进程没有控制终端(其终端名设置为?),因此,其在后台运行。大多守护进程都以超级用户权限执行


编程规则

(1)   首先调用umask将文件模式创建屏蔽字设置为一个已知数值(通常是0)。这样做是防止继承而来的屏蔽字没有某些权限,尤其是写权限。

(2)   调用fork,然后使父进程exit。

(3)   调用setsid创建一个新会话。

(4)   将当前工作目录更改为根目录。因为守护进程通常在系统引导之前就存在,如果守护进程的当前工作目录在一个需要挂载的文件系统上,那么该文件系统不能被卸载。也有某些守护进程会把当前工作目录更改到某个指定的位置,例如行式打印机假脱机守护进程就可能将其工作目录更改到它们的spool目录上。

(5)   关闭不再需要的文件描述符。可以使用open_max函数或者getrlimit函数获取最高文件描述符值,然后关闭直到该值的所有文件描述符。这样做可以避免守护进程从其父进程继承任何文件描述符。

(6)   某些守护进程将文件描述符0、1、2指向/dev/null。这样可以使得任何以恶搞试图读标准输入、写标准输出、写标准错误输出的程序不产生任何效果。由于守护进程是在后台运行的,因此登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们自然不希望在该终端上见到守护进程的输出,用户也不希望他们在终端上的输入被守护进程读取,因此上述措施是相当有用的
如下程序可由想要初始化为守护进程的程序调用,在main函数中调用函数daemonize,然后使main进程进入休眠状态,通过ps –efj命名查看进程状态,可以发现守护进程init,其终端名为 ?
[root@benxintuzi process]# cat init.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void daemonize(const char *cmd)
{
int                     i, fd0, fd1, fd2;
pid_t                   pid;
struct rlimit          rl;
struct sigaction       sa;

/*
*       * Clear file creation mask.
*               */
umask(0);

/*
*       * Get maximum number of file descriptors.
*               */
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
{
printf("%s: can't get file limit\n", cmd);
return ;
}

/*
*       * Become a session leader to lose controlling TTY.
*               */
if ((pid = fork()) < 0)
{
printf("%s: can't fork\n", cmd);
return ;
}
else if (pid != 0) /* parent */
exit(0);
setsid();

/*
*       * Ensure future opens won't allocate controlling TTYs.
*               */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
{
printf("%s: can't ignore SIGHUP\n", cmd);
return ;
}
if ((pid = fork()) < 0)
{
printf("%s: can't fork\n", cmd);
return ;
}
else if (pid != 0) /* parent */
exit(0);

/*
*       * Change the current working directory to the root so
*               * we won't prevent file systems from being unmounted.
*                       */
if (chdir("/") < 0)
{
printf("%s: can't change directory to \n", cmd);
}

/*
*       * Close all open file descriptors.
*               */
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; i++)
close(i);

/*
*       * Attach file descriptors 0, 1, and 2 to /dev/null.
*               */
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

/*
*       * Initialize the log file.
*               */
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
fd0, fd1, fd2);
exit(1);
}
}

int main(void)
{
daemonize("ps");
sleep(30);
return 0;
}

[root@benxintuzi process]# ./init
[root@benxintuzi process]# ps -efj
UID        PID  PPID  PGID   SID  C STIME TTY          TIME CMD
root         1     0     1     1  0 19:35 ?        00:00:03 /sbin/init
root      2198  2174  2198  2057  0 19:38 pts/0    00:00:00 vim init.c
root      2237     1  2236  2236  0 19:45 ?        00:00:00 ./init
root      2238  2188  2238  2100  0 19:45 pts/1    00:00:00 ps -efj


APUE 学习记录

      一个进程组可以包含多个进程

      进程组中的这些进程之间不是孤立的,他们彼此之间或者存在者父子、兄弟关系,或者在功能有相近的联系。

      那linux为什么要有进程组呢?其实提供进程组就是方便管理这些进程。

      假设要完成一个任务,需要同时并发100个进程,当用户由于某种原因要终止这个任务时,要是没有进程组,就需要一个个去杀死这些进程,设置了进程组之后,就可以对进程组中的每个进程进行杀死。  

     每个进程必定属于一个进程组,也只能属于一个进程组。  

     一个进程除了有进程ID外,还有一个进程组ID,每个进程组也有唯一的进程组ID。

     每个进程组有一个进程组组长,进程组组长的进程ID和组ID相同

函数 getpgrp 和 getpgid 可以返回调用进程的进程组ID

#include <unistd.h>

pid_t getpgrp(void);
pid_t getpgid(pid_t pid);
//返回值:成功则返回进程组ID,失败返回-1.


函数 setpgid 可以使进程加入现有的组或者创建一个新进程组。

#include <unistd.h>

int setpgid(pid_t pid, pid_t pgid );

setpgid将pid进程的进程组ID设置为pgid.
(1)pid=pgid ,则表示将pid指定的进程设为进程组组长
(2)pid=0 .则使用调用进程的进程ID
(3)pgid=0,则将pid指定进程用作进程组ID。


一个会话又可以包含多个进程组。一个会话对应一个控制终端


  

    linux是一个多用户多任务的分时操作系统,必须要支持多个用户同时登陆同一个操作系统,当一个用户登陆一次终端时就会产生一个会话,

  每个会话有一个会话首进程,即创建会话的进程,建立与终端连接的就是这个会话首进程,也被称为控制进程。一个会话可以包括多个进程组,

   这些进程组可被分为一个前台进程组和一个或多个后台进程组。为什么要这么分呢?前台进程组是指需要与终端进行交互的进程组(只能有一个)

  比如有些进程是需要完成IO操作的,那么这个进程就会被设置为前台进程组.当我们键入终端的中断键和退出键时,就会将信号发送到前台进程

  组中的所有进程。而 后台进程组是指不需要与终端进程交互的进程组,比如:一些进程不需要完成IO 操作,或者一些守护进程就会 被设置为后台进程组(可以有多个),

  (这是我的理解,不知道对错)。  如果终端接口检测到网络已经断开连接,则会将挂断信号发送给会话首进程。

            


进程调用 setsid 函数建立一个新会话.

#include <unistd.h
#include <unistd.h>

pid_t  setsid(pid_t pid);
//返回:成功则返回进程组ID,失败则返回-1.


如果调用次函数的进程不是进程组的组长,则会创建一个新会话,结果将发生下面3件事情:

(1)该进程会变为新会话的首进程。

(2)该进程会成为一个新进程组的组长进程

(3)该进程没有控制终端。

如果该调用进程已经是一个进程组的组长,则调用会出错。

为了保证不会出错,通常先fork一个子进程,在关闭父进程,因为子进程继承了父进程的进程组ID,

而进程iD则是新分配的,两者不可能相等,从而保证了子进程不会是进程组组长。(后面编写守护进程时会用到。)

怎样编写守护进程?

1. 在后台运行。 

为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。 

if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续


2. 脱离控制终端,登录会话和进程组 

有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 

控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长: 

setsid(); 

说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。 

3. 禁止进程重新打开控制终端 

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端: 

if(pid=fork())
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)


4. 关闭打开的文件描述符 

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们: 
for(i=0; i 关闭打开的文件描述符; close(i);)


5. 改变当前工作目录 

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/") 

6. 重设文件创建掩模 

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0); 

7. 处理SIGCHLD信号 

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。 

signal(SIGCHLD,SIG_IGN); 

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。 

daemontest.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
void init_daemon()
{
pid_t pid ;
int i ;
pid =fork();
if(pid<0)
{
printf("fork error secondly!\n");
exit(1);
}
else if(pid>0)//结束父进程
{
printf("this is first parent process!\n");
exit(0);
}//子进程继续运行
setsid() ;//前面为setsid正确调用提供了前提,使子进程成为新的会话组长和
//新的进程组长
pid=fork();
if(pid<0)//子进程成为无终端的会话组长,但是还是可以打开终端,为了
//使进程脱离终端,使之成为不是会话组长
{
printf(" fork error secondly!\n");
exit(1);
}
else if(pid>0)//关闭第一个子进程
{
printf("this is first child process!\n");
exit(0);
}//第二个子进程继续运行
for(i=0;i<NOFILE;i++)
{
close(i);
}
chdir("/tmp");
umask(0);
return;
}

main.c
#include <stdio.h>
#include <stdlib.h>
void init_daemon(void);

int main(void)
{
FILE *fp ;
init_daemon() ;
while(1)
{
if((fp=fopen("daemon.log","a"))>=0)
{
fprintf(fp,"%s","good");
fclose(fp);
sleep(10);
}

}
exit(0);
}


运行:

yuan@YUAN:~$ ./daemontest
this is first parent process!
this is first child process!
yuan@YUAN:~$ ps -axj


结果:

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
1  2970  2969  2969 ?           -1 S     1000   0:00 ./daemontest
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: