进程是程序的一次运行过程,除了进程虚拟地址空间和文件描述符等,进程控制块中还存放了进程运行的环境信息,包括用户、用户组、父进程、进程组和会话等。

###用户和用户组

//获得当前进程实际用户ID
pid_t getuid(void);
//获得当前进程有效用户ID
pid_t geteuid(void);
//获得当前进程实际用户组ID
pid_t getgid(void);
//获得当前进程有效用户组ID
pid_t getegid(void);

###进程和进程组 获得父子进程ID

//获得当前进程ID
pid_t getpid(void);
//获得父进程ID
pid_t getppid(void);

进程组

有时,为了完成某个工作,需多个进程参与协作,为便于管理,可以将多个进程定义为一个进程组。一个进程组包含一个以上的进程,领头进程的进程ID等于进程组ID,进程组中不包含进程时,进程组自动消失。

会话

会话用于标识用户登录的每一个终端,每个登录终端都有一个会话ID与其对应; 会话包括控制进程(与终端建立连接的领头进程)、一个前台进程组和任意后台进程组。一个会话只能有一个控制终端,通常是登录到其上的终端设备或伪终端设备,产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程。

如果调用setsid函数的进程不是进程组中的领头进程,则可建立新的会话,(可在子进程中建立新的会话),该进程成为领头会话,同时产生一个新的进程组,且该进程为新进程组的领头进程,但不拥有终端。

/*
获得进程所属会话ID
成功返回会话ID,错误返回-1
*/
pid_t getsid(pid_t pid);
/*
创建一个新的会话,使进程组ID等于该会话ID
成功返回新的进程组ID,否则-1
*/
pid_t setsid(void);

案例:在子进程中创建新的领头会话

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int p,pid;
    printf("now session id %d\n\n",getsid(getpid()));
    p = fork();
    if(p)       //父进程退出
        exit(0);
    pid = setsid();
    printf("new session id  %d\n",pid);
    return pid;
}

###守护进程

守护进程是一种运行在后台,且不受任何终端影响的进程,因此,需要关闭标准输入、标准输出、标准错误输出的文件描述符.通常守护进程以服务进程的形式存在,例如web服务器。

同时要使守护进程脱离用户环境,所以要将工作目录修改为系统工作目录。 创建守护进程步骤:

  1. 创建子进程后结束父进程
  2. 在子进程中建立新的领头会话
  3. 修改工作目录和权限掩码信息
  4. 关闭文件描述符0,1,2

案例:创建一个守护进程

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>

int daemon_init()
{
    pid_t pid;
    char buf[80];
    FILE * fout;
    if((pid = fork()) < 0)
        return -1;
    else if(pid != 0)       
        exit(0);            //结束父进程
    setsid();               //创建领头会话
    system("cd /");         //改变工作目录
    umask(0);               //清除权限掩码
    close(0);               //关闭文件描述符
    close(1);
    close(2);
    getcwd(buf,sizeof(buf));
    fout = fopen("/tmp/result.txt","w");
    
    fprintf(fout,"work dirctory is %s\n",buf);
    fprintf(fout,"daemon pid is %d\n",getpid());
    fprintf(fout,"daemon parent pid is %d\n",getppid());
    fclose(fout);
    return 0;
}

int main()
{  
    FILE * fout;
    printf("start init daemon ...");
    daemon_init();
    while(1)
    {
        fout = fopen("/tmp/result.txt","a");
        fputs("i am still alive\n",fout);
        fflush(fout);
        sleep(1);
    }
}

###加载可执行映像

可执行映像是链接好的可执行的代码。

通常,子进程创建时,继承了父进程的资源,父子进程可以并发运行,它们由同一代码流程控制,具有相似行为。有时,希望子进程拥有独立代码流程,可以通过加载可执行二进制映像文件来实现。内核通过exec系统调用在进程中建立新的运行环境。

ELF格式

Linux系统中,采用ELF(Excutable and Linkable Format),ELF有3中基本格式

  1. 可执行格式
  2. 目标文件(.o文件)
  3. 共享库(.so文件)

加载可执行文件

ELF的可执行文件的加载是通过系统调用exec完成的,当进程调用exec函数加载ELF可执行文件时,exec将以新加载程序的段替换当前进程的相应的正文、数据、堆和栈段;同时保留大部分的进程属性。例如进程ID、父进程ID、进程组ID、实际用户ID、会话ID、当前目录、文件描述符等。

但当加载可执行文件的SETUID或SETGID位被设置,进程的有效用户ID和有效用户组ID被设置为该文件的属主ID和属主用户组ID。 exec相关函数原型:

#include <unistd.h>
int execl(const char *path,const char *arg,...)
int execv(.........).........
int execle(........)
int execve(.........)
int execlp(.........)
int execvp(.........)