Linux多任务编程

Linux下多任务机制的介绍

多任务处理是指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务。Linux就是一个支持多任务的操作系统,比起单任务系统它的功能增强了许多。
多任务操作系统使用某种调度策略支持多个任务并发执行。事实上,(单核)处理器在某一时刻只能执行一个任务。每个任务创建时被分配时间片(几十到上百毫秒),任务执行(占用CPU)时,时间片递减。操作系统会在当前任务的时间片用完时调度执行其他任务。由于任务会频繁地切换执行,因此给用户多个任务同时运行的感觉。多任务操作系统中通常有3个基本概念:任务、进程和线程。

任务

任务是一个逻辑概念,指由一个软件完成的活动,或者是为实现某个目的的一系列操作。通常一个任务是一个程序的一次运行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务是进程或者是线程。例如,一个杀毒软件的一次运行是一个任务,目的是在各种病毒的侵害中保护计算机系统,这个任务包含多个独立功能的子任务(进程或线程),包括实时监控功能、定时查杀功能、防火墙功能以及用户交互功能等。任务、进程和线程之间的关系如图所示。
在这里插入图片描述

进程

1.进程的基本概念
进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,它是操作系统进行资源分配和调度的基本单元。一次任务的运行可以激活多个进程,这些进程相互合作来完成该任务的一个最终目标。
进程具有并发性、动态性、交互性和独立性等主要特性。
(1)并发性)指的是系统中多个进程可以同时并发执行,相互之间不受干扰。
(2)动态性。指的是进程都有完整的生命周期,而且在进程的生命周期内,进程的状态是不断变化的,另外,进程具有动态的地址空间(包括代码、数据和进程控制块等)。
(3)交互性。指的是进程在执行过程中可能会与其他进程发生直接和间接的通信,如进程同步和进程互厅等,需要为此添加一定的进程处理机制。
(4)独立性。指的是进程是一个相对完整的资源分配和调度的基本单位,各个进程的地址空间是相互独立的,只有采用某些特定的通信机制才能实现进程之间的通信。
进程和程序是有本质区别的。程序是一段静态的代码,是保存在非易失性存储器上的指令和数据的有序集合,没有任何执行的概念;而进程是一个动态的概念,它是程序的一次执行过程,包括了动态创建、调度、执行和消亡的整个过程,它是程序执行和资源管理的最小单位。

Linux系统中主要包括下面儿种类型的进程。
(1)交互式进程.这类进程经常与用户进行交互,需要等待用户的输入(键盘和鼠标操作等)。当接收到用户的输入之后,这类进程能够立刻响应。典型的交互式进程有shell命令进程、文本编辑器和图形应用程序运行等。
(2)批处理进程。这类进程不必与用户进行交互,因此通常在后台运行。因为这类进程通常不必很快地响应,因此往往不会优先调度。典型的批处理进程是编译器的编译操作、数据库搜索引擎等。
(3)守护进程。这类进程一直在后台运行,和任何终端都不关联。通常系统启动时开始执行,系统关闭时才结束。很多系统进程(各种服务)都是以守护进程的形式存在。

2.Linux下的进程结构
进程不但包括程序的指令和数据,而且包括程序计数器和处理器的所有寄存器以及存储临时数据的进程堆栈。
因为Linux是一个多任务的操作系统,所以其他的进程必须等到操作系统将处理器使用权分配给自已之后才能运行。当正在运行的进程需要等待其他的系统资源时,Linux内核将取得处理器的控制权,按照某种调度算法将处理器分配给某个正在等待执行的进程。
内核将所有进程存放在双向循环链表(进程链表)中,链表的每一项都是Iaskstruct,称为进程控制块的结构。该结构包含了与一个进程相关的所有信息,在<include/Linux/sched.h>文件中定义.task_struct内核结构比较大,它能完整地描述一个进程,如进程的状态、进程进程的状态的基本信息,进程标识符、内存相关信息、父进程相关信息、与进程相关的终端信息、当前工作目录、打开的文件信息、所接收的信号信息等。

下面详细讲解task_struct结构中最为重要的两个域:state(进程状态)和pid(进程标识符)。
(1)进程状态。
Linux中的进程有以下几种主要状态。
1)运行状态(TASK_RUNNING)。进程当前正在运行,或者正在运行队列中等待调度。
2)可中断的阻塞状态(TASK_INTERRUPTIBLE)。进程处于阻塞(睡眼)状态,正在等待某些事件发生或能够占用某些资源。处在这种状态下的进程可以被信号中断。接收到信号或被显式地唤醒呼叫(如调用wake_up系列宏:wake_up、wake_up_interruptible等)唤醒之后,进程将转变为TASK_RUNNING状态。
3)不可中断的阻塞状态TASK_UNINTERRUPTIBLE)。此进程状态类似于可中断的阻塞状态(TASK_INTERRUPTIBLE),只是它不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在一些特定的情况下(进程必须等待,直到某些不能被中断的事件发生),这种状态是很有用的。只有在它所等待的事件发生时,进程才被显式地唤醒呼叫唤醒

4)暂停状态TASK_STOPPED)。进程的执行被暂停,当进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号,就会进入暂停状态。
5)僵死状态EXIT_ZOMBIE)。子进程运行结束,父进程未退出,并且未使用wait函数族(如使用waitpid()函数)等系统调用来回收子进程的退出状态。处在该状态下的子进程已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其父进程收集。
6)消亡状态(EXIT_DEAD)。这是最终状态,父进程进程状态的转换调用wait函数族回收之后,子进程彻底由系统删除,不可见。

在这里插入图片描述

内核可以使用set_task_state和set_current_state宏来改变指定进程的状态和当前执行进程的状态。
(2)进程标识符。
Linux内核通过唯一的进程标识符PID来标识每个进程.PID存放在task_struct的pid字段中。系统中可以创建的进程数目有限制,读者可以查看/proc/sys/kernel/pid_max来确定上限。
当系统启动后,内核通常作为某一个进程的代表。一个指向task_struct的宏current用来记录正在运行的进程。current经常作为进程描述符结构指针的形式出现在内核代码中,例如,current→pid表示处理器正在执行的进程的PID.当系统需要查看所有的进程时,则调用for_each_process()宏,这将比系统搜索数组的速度要快得多。在Linux中获得当前进程的进程号(PID)和父进程号(PPID)的系统调用函数分别为getpid()和getppid()。
3.进程的创建、执行和终止
(1)进程的创建和执行。
许多操作系统都提供产生进程的机制,也就是首先在新的地址空间里创建进程,读入可执行文件,最后再开始执行,Linux中进程的创建很特别,它把上述步骤分解到两个单独的函数中执行:fork()和exec函数族。首先,fork()通过复制当前进程创建一个子进程,子进程与父进程的区别仅仅在于不同的PID,PPID和某些资源及统计量.exec函数族负责读取可执行文件并将其载入地址空间开始运行。要注意的是,Linux中的fork()使用的是写时复制(copyonwrite)的技术,也就是内核在创建进程时,其资源并没有立即被复制过来,而是被推迟到需要写入数据的时候才发生。在此之前只是以只读的方式共享父进程的资源。写时复制技术可以使Linux拥有快速执行的能力,因此这个优化是非常重要的.
(2)进程的终止。
进程终止也需要做很多烦琐的收尾工作,系统必须保证进程所占用的资源回收,并通知父进程。Linux首先把终止的进程设置为僵死状态。这个时候,进程已经无法运行,它的存在只为父进程提供信息。父进程在某个时间调用wait函数族,回收子进程的退出状态,随后子进程占用的所有资源被释放。
4.进程的内存结构
Linux操作系统采用虚拟内存管理技术,使得每个进程都有独立的地址空间。该地址空间是大小为4GB的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但更安全(用户不能直接访问物理内存),而且用户程序可以使用比实际物理内存更大的地址空间。
4GB的进程地址空间会被分成两个部分一用户空间与内核空间。用户地址空间是0~3GB(0xC0000000),内核地址空间占据3~4GB。用户进程在通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程使用系统调用(代表用户进程在内核态执行)时才可以访问到内核空间。每当进程切换,用户空间就会跟着变化;而内核空间是由进程的内存布局内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。每个进程的用户空间都是完全独立、互不相干的。进程的虚拟内存地址空间如图3.3所示。
用户空间包括以下几个功能区域(通常也称之为“段(segment))。
(1)只读段。具有只读属性,包含程序代码(.init和.text)和只读数据(.rodata)。
(2)数据段。存放的是全局变量和静态变量。其中初始化数据段(.data)存放显示初始化的全局变量和静态变量,未初始化数据段,此段通常被称为BSS段(.bss),存放未进行显示初始化的全局变量和静态变量。
在这里插入图片描述
(3)栈。由系统自动分配释放,存放函数的参数值,局部变量的值、返回地址等。
(4)堆。存放动态分配的数据,一般由程序员动态分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
(5)共享库的内存映射区域。这是Linux动态链接器和其他共享库代码的映射区域。
·因为在Linux系统中每一个进程都会有“/proc”文件系统下的与之对应的一个目录(例如,init进程的相关信息存放在“/proc/1”目录下),因此通过proc文件系统可以查看某个进程的地址空间的映射情况.例如,运行一个应用程序,如果它的进程号为13703,则输入“cat/proc/13703/maps该进程的内存映射情况

线程

前面已经提到,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段、代码段和堆栈段,这就造成了进程在进行切换时操作系统的开销比较大。为了提高效率,操作系统又引入了另一个概念
线程,也称为轻量级进程。线程可以对进程的内存空间和资源进行访问,并与同一进程中的其他线程共享。因此,线程的上下文切换的开销比进程一小得多。
一个进程可以拥有多个线程,其中每个线程共享该进程所拥有的资源。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响。由此可知,多线程中的同步是非常重要的问题。在多线程系统中,进程与进程的关系如图3.4所示。

在这里插入图片描述

##进程编程
###进程编程基础
1.fork()
在Linux中创建一个新进程的方法是使用fork()函数。fork()函数是Linux中一个非常重要的函数,和读者以往遇到的函数有一些区别,因为它看起来执行一次却返回两个值.一个函数真的能同时返回两个值吗?希望读者能认真地学习下面的内容。
(1)fork()函数说明。
fork()函数用于从已存在的进程中创建一个新进程.新进程称为子进程而原进程称为父进程.使用fork()函数得到的子进程是父进程的一个复制品它从父进程处继承了整个进程的地址空间,包括进程上下文,代码段,进程堆栈、内存信息、打开的文件描述符、信号处理函数、进程优先级,进程组号,当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号,资源使用和计时器等
因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序。因此需要用一种方式来区分它们,并使它们照此运行,否则,这两个进程只能做相同的事。
父子进程一个很重要的区别是:fork()的返回值不同。父进程中的返回直是子进程的进程号,而子进程中返回0。可以通过返回值来判定该进程是父进程还是子进程。
子进程没有执行fork()函数,而是从fork()函数调用的下一条语句开始执行
在这里插入图片描述
fork()函数的简单的示例程序如下

intmain(void)
{
/*调用fork()函数*/
id_tret:
ret=fork();
/*通过ret的值来判断fork()函数的返回情况,首先进行出错处理*/
if(ret==-1)
{
perror("forkerror");
return-l;
}elseif(ret==0)/*返回值为0代表子进程*/
{
printf("Inchildprocess!!retis%d,MyPIDisd\n",ret,getpid());
}else/*返回值大于0代表父进程*/
{
printf("Inparentprocess!!retis%d,MyPIDis&d\n",ret,getpidO);
}
returnO;
}

2.exec函数族
1)exec函数族说明。
fork()函数用于创建一个子进程,该子进程几乎复制了父进程的全部内容。我们能否让子进程执行一个新的程序呢?exec函数族就提供了一个在进程中执行另一个程序的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代当前进程的数据段、代码段和堆栈段。在执行完之后,当前进程除了进程号外,其他内容都被替换了。这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。
在Linux中使用exec函数族主要有两种情况。
①当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用exec函数族中的任意一个函数让自己重生。
②如果一个进程想执行另一个程序,那么它就可以调用fork(函数新建个进程,然后调用exec函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普追).
2)exec函数族语法。
实际上,在Linux中并没有exec()函数,而是有6个以exec开头的函数,它们之间在语法上有细微差别,下面会详细讲解。
在这里插入图片描述

这6个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数传递方式及环境变量这几个方面进行比较。
①查找方式。读者可以注意到,表3.2中的前4个函数的查找方式都是完整的文件目录路径,而最后两个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量“PATH”所包含的路径进行查找。
②参数传递方式。exec函数族的参数传递方式有两种:一种是逐个列举的方式,另一种是将所有参数通过指针数组传递。在这里是以函数名的第5位字母来区分的,字母为l(list)的表示逐个列举参数的方式,其类型为constchararg;字母为v(vertor)的表示通过指针数组传递,其类型为charconstargv[]。读者可以观察execl()、execle()、execlp()的语法与execv()、execve()、execvp()的区别。它们具体的用法在后面的实例讲解中会具体说明。
这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL结尾。
③环境变量。exec函数族可以使用默认的环境变量,也可以传入指定的环境变量。这里以e(environment)结尾的两个函数execle(和execve(就可以在envp口中指定当前进程所使用的环境变量。
在这里插入图片描述

事实上,这6个函数中真正的系统调用只有execve0,其他5个都是库函数,它们最终都会调用execve()这个系统调用。在使用exec函数族时,一定要加上错误判断语句。exec很容易执行失败,其中最常见的原因如下。
·找不到文件或路径,此时errno被设置为ENOENT。
·数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT.
·没有对应可执行文件的运行权限,此时errno被设置为EACCES。
(3)exec使用实例。
下面的第一个示例说明了如何使用文件名的方式来查找可执行文件,同时使用参数列表的方式,这里用的函数是execlp())。

/*execlp.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
intmain()
{
    if(fork(==0))
    {
        /*调用execlp函数,这里相当于调用了"ps-ef"命令*/
        if((ret=execlp("ps",“ps","-ef",NULL))<O)
        {
            printf("execlperror\n");
        }
    }
}

在该程序中,首先使用fork()函数创建一个子进程,然后在子进程里使用execlp()函数。此程序的运行结果与在 shell 中直接输入命令 “ ps -ef ” 是一样的。

/*execl.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
    if(fork()==O)
    /*调用,execl,0函数,,注意这里要给出,ps,程序所在的完整路径,*/
    if(execl("/,bin/,ps",",ps",,"-ef,",NULL)<O)
    {
        printf("execl,error\n");
    }
}

下面的示例利用函数execle(将环境变量添加到新建的子进程中,这里的env是查看当前进程环境变量的命令。

/*execle.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
    /*命令参数列表,必须以NULL结尾*/
    char*envpll={"PATH=/tmp","USER=harry",NULL);
    if(fork()==0)
    /*调用execle()函数,注意这里也要指出env的完整路径*/
    if(execle("/usr/bin/env",“eny",NULL.envp)<O)
    {
        printf("execle error\n")
    }
}
$./execle
PATH=/tmp
USER=harry

最后一个示例使用execve()函数,通过构造指针数组的方式来传递参数,注意参数列表一定要以NULL作为结尾标识符。其代码如下。

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
    /*命令参数列表,必须以NULL结尾*/
    char *arg[]={"env",NULL};
    char* envp[]={"PATH=/tmp","USER=harry",NULL};
    if(fork()==O)
    if(execve("/usr/bin/env",arg,envp)<0)
    {
         printf("execveerror\n");
    }
   
}
$.execve
PATH=/tmp
USER=harry

在这里插入图片描述

从图 3.5 中可以看出, _exit()函数的作用是直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;exit()函数则在这些基础上做了一些包装, 在执行退出之前加了若干道工序。 exit()函数与 _exit()函数最大的区别就在于exit()函数在终止当前进程之前要检查该进程打开了哪些文件,并把文件缓冲区中的内容写回文件,就是图中的“清理 I/O 缓冲”一项。
由于在 Linux的标准函数库中 ,有一种被称为“缓冲 I/O(buffered I/O)”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。
每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写人文件。
这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。比如有些数据,认为已经被写入文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭掉,缓冲区中的数据就会丢失。 因此 , 若想保证数据的完整性, 最好使用 exit()函数。

在这里插入图片描述

在这里插入图片描述

( 3 ) exit()和 _exit()使用示例 。
这两个示例比较了 exit()和 _exit()两个函数的区别。 标准输出流 stdout 是行缓冲,遇到 “\n” 换行符时才会实际写入终端。示例中就是利用这个性质来进行比较的。 示例 1 的代码如下。

/* exit.c */
#include <st dio.h> 
#include <stdlib.h>
int main()
{
    printf(" Using exit...\n") ;
    printf(" This is the content in buffer "); 
    exit(0) :
}
/** 
$ ./exit
Using exit...
This is the content in bu ffer $
*/

读者从输出的结果中可以看到,调用 exit() 函数时,缓冲区中的内容也能正常输出

/* _exit.c */
#include <st dio.h> #include <unistd.h>
int main()
{
    printf ( " Using _exit...n" );
    printf ( " This is the content in buffer");
    /* 加 上回车符之后结果又如何*/
    _exit(0);
}
/*
Using _ exit..
*/

可以看到, 调用_exit()进程结束时没有输出缓冲区中的内容。

4.wait()和waitpid()
(1)wait()和waitpid()函数说明。
wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号为止。如果该父进程没有子进程或者他的子进程已经结束,则wait()会立即返回-1。
waitpid()的作用和wait()一样,但它并不一定等待第一个终止的子进程。waitpid()有若干选项,可提供一个非阻塞版本的wait()功能。实际上wait()函数只是waitpid()函数的一个特例,在Linux内部实现wait(函数时直接调用的就是waitpid(函数。
(2)wait()和waitpid()函数格式说明。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

(3)waitpid()使用示例 。
由于 wait() 函数的使用较为简单,在此以 waitpid()为例进行讲解。 本例中首先使用 fork()创建一个子进程, 然后让子进程暂停 5s(使用了 sleep()函数()。 接下来对原有的父进程使用 waitpid()函数, 并使用参数W NOHANG 使该父进程不会阻塞。若有子进程退出 ,则 waitpid()返回子进程号;若没有子进程退出则 waitpid ()返回 0, 并且父进程每隔 1s 循环判断一次。 该程序的流程图如图 3.6 所示。
在这里插入图片描述

该程序源代码如下。

/*waitpid. c*/
#include <sys/types.h>
#include <sys/wait.h> 
#include <unistd.h> 
#include <stdio.h>
#include <stdlib.h>

int main()
{
    pid_t pid , ret ;
    if((pid=fork())<0)
    {
        printf("Error fork\n ") ;
    }
    else if (pid == 0 ) /* 子进程 */
    {
        /*子进程暂停 5s*/ 
        sleep (5) :
        /*子进程正常退出 */ 
        exit(0) ;
    }
    else
    {
        /* 循环测试子进程是否退出*/
        do
        {
            /* 调用 waitpid, 且父进程不阻塞 */
            ret = waitpid(pid,NULL, WNOHANG) ;
            /* 若子进程还未退出 , 则父进程暂停1s */
            if (ret == O)
            {
                printf("The child process has not exited\n");
                sleep(1)
            }
        }while(ret==0);
        /*广若发现子进程退出,打印出相应情况·*/
        if(pid e = ret )
        {
            printf("child process exited \n" );
        }
        else
        {
            printf("some error occured.\n" );
        }
    }
}

如果把 ret = waitpid( pid,NULL, WNOHANG); 改为 ret = waitpid(pid,NULL,0);父进程会一直阻塞,直到子进程结束为止.

Linux 守护进程

  1. 守护进程概述
    守护进程也就是通常所说的 Daemon进程,是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统启动时开始执行,在系统关闭时终止。 Linux 中很多系统服务都是通过守护进程实现的。
    在 Linu x 中, 每一个系统与用户进行交流的界面称为终端。 每一个从此终端开始运行的进程都会依附于该终端,这个终端称为这些进程的控制终端。当控制终端关闭时,相应的进程都会自动结束。但是守护进程却能够突破这个进程不因为用户,终端或者其他的变化而受到影响,那么把这个进程变成一个守护进程。
  2. 编写守护进程
    编写守护进程看似复杂,但实际上也是遵循一个特定的流程。下面就分 5个步骤来讲解怎样创建一个简单的守护进程。在讲解的同时,会同时介绍与创建守护进程相关的概念和函数,希望读者能很好地掌握。
    ( 1) 创建子进程,父进程退出。
    这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后子进程变成后台进程,给用户感觉程序已经运行完毕。之后的所有工作都在子进程中完成, 而用户通过 shell 可以执行其他的命令, 从而在形式上做到了与控制终端的脱离。
    到这里,有些读者可能会问,父进程创建了子进程之后退出,此时该子进程不就没有父进程了吗? 守护进程中确实会出现这么一个有趣的现象。由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程。在 Linux 中,每当系统发现一个孤儿进程,就会自动由 1号进程(也就是 init 进程)收养它,这样,原先的子进程就会变成 init 进程的子进程了。其实现代码如下。
pid = fork();
if(pid>0)
{
	 exit();//父进程退出	
}

(2)在子进程中创建新会话。
这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常重大。 在这里使用的函数是 setsid ()。 在具体介绍 setsid()之前,读者首先要了解两个概念:进程组和会话期。
① 进程组。 进程组是一个或多个进程的集合。进程组由进程组 ID 来唯一标识。 除了进程号 ( PID )之外,进程组 ID 也是一个进程的必备属性。
每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID,且进程组 ID 不会因组长进程的退出而受到影响。
② 会话期。会话组是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出;或者说开始于终端打开,结束于终端关闭。会话期的第一个进程称为会话组长。在此期间该用户运行的所有进程都属于这个会话期, 它们之间的关系如图 3.7所示。

setsid()有下 面 3 个 作用 。
① 让进程摆脱原会话的控制。
② 让进程摆脱原进程组的控制。
③ 让进程摆脱原控制终端的控制。
那么,在创建守护进程时为什么要调用 setsid()函数呢?创建守护进程的第一步,在那里调用了 fork()函数来创建子进程再令父进程退出。 由于在调用 fork()函数时,子进程全盘复制了父进程的会话期、进程组和控制终端等。虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此, 还不是真正意义上的独立, 而 setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
(2) setsid()函数格式 。
在这里插入图片描述
在这里插入图片描述

(3)改变当前目录。
这一步也是必要的步骤。 使用 fork()创建的子进程继承了父进程的当前工作目录。 由于在进程运行过程中,当前目录所在的文件系统( 比如 “ / mnt/usb 等)是不能卸载的,这对以后的使用会造成诸多的麻烦 (比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让根目录作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径, 如 “ /tmp ”。 改变工作目录的函数是 chdir()。
(4)重设文件权限掩码。
文件权限掩码(通常用八进制表示)的作用是屏蔽文件权限中的对应位例如,如果文件权限掩码是 050,它表示屏蔽了文件组拥有者的可读与可拱行权限。 由于使用 fork()函数新建的子进程继承了父进程的文件权限掩码,
这就给该子进程使用文件带来了一定的影响。 因此,把文件权限掩码设置为 0 , 可以增强该守护进程的灵活性。 设置文件权限掩码的函数是 umask()。在这里, 通常的使用方法为 umask(0)。
(5)关闭文件描述符。
同文件权限掩码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程访问,但它们一样占用系统资源,而且还可能导致所在的文件系统无法被卸载。
特别是守护进程和终端无关,所以指向终端设备的标准输入、标准输出和标准错误流已经失去了存在的价值,应当被关闭。通常按如下方式关闭文件描述符。

int num;
num = getdtablesize();//获取当前进程文件描术符表大小
for(i=0;i<num;i++)
{
	close(i);	
}

在这里插入图片描述

下面是实现守护进程的一个完整示例,该示例首先按照以上的创建流程建立了一个守护进程,然后让该守护进程每隔 2s 向日志文件“ /tmp/ daemon.log”写入字符串。

/*dae mon.c */
/*创建守护 进程实例 */
#include<stdio.h>
#include <stdlib.h>
#include<string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h >
#include <sys/wait.h>


int main()
{
        pid_t pid;
        int i,fd;
        char *buf =  "This is a Daemon\n";

        pid = fork();/* 第一步 */

        if(pid<0)
        {
                printf("Error fork\n");
                exit(1);
        }
        else if(pid>0)
        {
                exit(0);/*父进程退出*/
        }
        setid();/*第二步*/
        chdir("/tmp");/*第三步*/
  		umask(0);/*第四步*/
        for(i=0;i<getdtablesize();i++)/*第五步*/
        {
                close(i)
        }

        /*这时创建完守护进程,以下开始正式进入守护进程工作*/
        while(1)
        {
                if ((fd = open(" daemon.log",O_CREAT|O_WRONLY|O_TRUNC, 0600)) < 0)
                {
                        printf("Open file error \n");
                        exit(1);
                }
                write(fd,buf,strlen(buf));
                close(fd);
                sleep(2);
        }
        exit(0);

}
  1. 守护进程的出错处理
    读者在前面编写守护进程的具体调试过程中会发现, 由于守护进程完全脱离了控制终端,因此,不能像其他普通进程一样将错误信息输出到控制终端。那么,守护进程要如何调试呢? 一种通用的办法是使用 syslog 服务,将程序中的出错信息输入到系统日志文件中 (如 “ /var /log /messa ges ” ),从而可以直观地看到程序的问题所在 (“/var/log/messages ” 系统日志文件只能由拥有 root 权限的超级用户查看.在不同Linux 发行版本中,系统日志文件路径全名可能有所不同,例如, 可能是 “ /var/log/syslog ”)syslog 是 Linu x 中的系统日志管理服务 ,通过守护进程 syslogd 来维护。该守护进程在启动时会读一个配置文件 “/etc/syslog.conf ”。 该文件决定了不同种类的消息会发送向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可被记录到一个文件中。
    该机制提供了3个syslog 相关函数,分别为 openlog(),syslog ()和 closelog().
    (1)相关函数说明。
    通常,openlog()函数用于打开系统日志服务的一个连接;syslog()函数用于向日志文件中写人消息,在这里可以规定消息的优先级、消息输出格式等; closelog()函数用于关闭系统日志服务的连接。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

(3)使用示例。
这里将上一节中的示例程序用 sy slog 服务进行重写,其中有区别的地方用加粗的字体表示, 源代码如下

/* syslog_daemon.c利用 syslog 服务的守护 进程实例 */ 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <syslog.h>

int main()
{
        pid_t pid , sid ;
        int i,fd;
        char* buf = "This is a Daemon \n";
        pid = fork(); /* 第一步 */
        if(pid < 0)
        {
                printf( "Error fork\n");
                exit(1);
        }
        else if(pid > 0)
        {
                exit(0)/* 父进程退出 */
        }
        /*打开系统日志服务:  openlog*/
        openlog("daemon_syslog", LOG_PID,LOG_DAEMON);
        if(( sid = setsid())< O) / *第二 步 *
        {
                syslog(LOG_ERR, "%s\n", "setsid");
                exit(1);
        }
        if((sid = chdir(" /"))< O) /*第三步*/
        {
                syslog(LOG_ERR, "%s\n", "chdir");
                exit(1);
        }
        umask(0); /*第四步*/
        for(i = O ; i < getdtablesize();i++)/*第五步*/
        {
                close(i)
        }
        /*这时创建完守护进程,以下开始正式进入守护进程工作*/
        while(1)
        {
                if((fd = open("/tmp/daemon.log",O_CREATIO_WRONLY|O_APPEND,0600)) < O )
                {
                        syslog(LOG_ERR,"open");
                        exit(1);
                }
                write(fd, buf , strlen(buf)+ 1);
                close(fd);
                sleep(10);
        }
        closelog();
        exit(0);
}

读者可以尝试用普通用户的身份执行此程序,由于这里的 open()函数必须具有 root 权限,因此,syslog 就会将错误信息写入系统日志文件( 如 " /var /log / messages ” ) 中 , 信息如下。
Jan 30 18: 20: 08 localhost daemon _syslog[6121 : open

编写多进程程序

  1. 目的
    通过编写多进程程序 ,读者应熟练掌握 fork ()、exec()、wait()和 waitpid() 等函数的使用,进一步理解在 Linux 中多进程编程的步骤。
  2. 内容
    该实验有 3个进程,其中一个为父进程,其余两个是该父进程创建的子进程。 其中一个子进程运行 “ls -l” 指令,另一个子进程在暂停 5s 之后异常退出,父进程先用阻塞方式等待第一个子进程的结束,然后用非阻塞方式等待另一个子进程的退出,待收集到第二个子进程结束的信息,父进程就返回。
  3. 步骤
    ( 1 ) 画出该实验流程图 。
    该实验流程图如图 3.9 所示。
    在这里插入图片描述

(2)源代码。
先看一下下面的代码,这个程序能得到我们所希望的结果吗? 它的运会产生几个进程? 请读者回忆一下 fork()调用的具体过程 。

/*multi_proc_wrong.c*/
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
        pid_t child1,child2,child;
        /*创建两个子进程*/
        child1 = fork();
        child2 = fork();
        /*子进程1的出错处理*/
        if(child == -1)
        {
                printf("Child1 fork error\n");
                exit(1);
        }
        else if(child1 == 0)
        {
                prinf("In child1:execute 'ls -l'\n");
                if(execlp("ls","ls","-1",NULL)<0)
                {
                        printf("Child1 execlp error\n");
                }
        }

        if(child2 == -1)
        {
                printf("Child2 fork error\n");
                exit(1);
        }
        else if(child2 == 0)
        {
                printf("In child2:sleep for 5 seconds and then exit\n");
                sleep(5);
                exit(0);
        }
        else/*在父进程中等待两个子进程的退出*/
        {
                printf("In father process : \n");
                child = waitpid(child1,NULL,O); /* 阻塞式等待 */
                if(child == child1)
                {
                        printf("Get childl exit code\n ");
                }
                else
                {
                        printf("Error occured! \n")
                }

        }
        do
        {
                child = waitpid(child2,NULL,WNOHANG);/* 非阻塞式等待 */
                if(child == 0)
          {
                        printf("The child2 process has not exited!\n ")
                                sleep(1);
                }

        } while(child == O)
        if(child == child2)
        {
                printf("Get child2 exit code\n ");
        }
        else
        {
                printf("Error occured i<n" );
        }
        exit(0);

}
                                           

编译和运行以上代码,并观察其运行结果。

/ * multi _ proc.c * / 
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> 
#include <unistd.h>
#include <sys/wait.h>

int main()
{
        pid_t child1,child2,child;
        /*创建两个子进程 */
        child1 = fork();
        /*子进程1的出错处理*/
        if(childl == -1 )
        {
                printf(" Child1 fork error\n ");
                exit(1);
        }
        else if(childl == 0)/* 在子进程 1 中调用 execlp()函数 */
        {   
                printf("In child1 : execute 'ls -l'\n " ) ; 
                if(execlp("Is""Is""-I"NULL) < O)
                {
                        printf(" Child1 execlp error \n ");
                }
        }   
        else /* 待两个子进程的退出*/
        {   
                child2 = fork();
                if(child2 == -1) /*子 进程 2的 出 错处理 */
                {
                        printf("Child2 fork error \n");
                        exit(1);
                }
                else if(child2 == 0)/* 在子进程 2中使其暂停 5 s */
                {
                        printf("In child2: sleep for 5 seconds and then exit\n ");
                        sleep(5);
                        exit(0);
                }
                printf(" In father process : \n ");
                //(以下部分跟前面程序的父进程执行部分相同)
        }
        exit(0);
}

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐