Linux进程与子进程

时间:2021-11-07 15:48:50

Linux进程

进程是Linux中事务管理的基本单元,所有的进程都拥有自己的独立处理环境和系统资源,并且各进程之间不可以直接访问对方的资源,进程之间的交流需要通过特定的机制(IPC)。

Linux系统的内核头文件中(/usr/src/kernels/内核版本/include/linux/sched.h)定义了进程控制块(PCB)结构体struct task_struct来管理每个进程的资源。

进程资源分为两个部分:内核空间进程资源、用户空间进程资源

内核空间进程资源:PCB的相关信息,控制块本身、打开的文件表项、当前目录、当前终端信息、线程基本信息、可访问的内存空间、PID、PPID、UID、EID等。

用户进程空间资源:通过成员mm_struct映射的内存空间,实际上就是进程的代码段、数据段、堆、栈、可访问的共享库内存空间。这些资源在进程退出的时候主动释放。在进程运行时,可以通过查

看/proc/{pid}/maps文件查看可以访问的地址空间。

进程的状态:就绪、运行、等待(可中断、不可中断)、停止、僵死

在/usr/src/kernels/内核版本/include/linux/sched.h头文件中定义了进程的状态

/*
 * Task state bitmask. NOTE! These bits are also
 * encoded in fs/proc/array.c: get_task_state().
 *
 * We have two separate sets of flags: task->state
 * is about runnability, while task->exit_state are
 * about the task exiting. Confusing, but this way
 * modifying one set can't modify the other one by
 * mistake.
 */
#define TASK_RUNNING		0
#define TASK_INTERRUPTIBLE	1
#define TASK_UNINTERRUPTIBLE	2
#define __TASK_STOPPED		4
#define __TASK_TRACED		8
/* in tsk->exit_state */
#define EXIT_ZOMBIE		16
#define EXIT_DEAD		32
/* in tsk->state again */
#define TASK_DEAD		64
#define TASK_WAKEKILL		128
#define TASK_WAKING		256
#define TASK_PARKED		512
#define TASK_STATE_MAX		1024
进程之间状态的转换图如下:

Linux进程与子进程

进程的属性

进程本身的属性:PID(进程号)、PPID(父进程号)、PGID(进程组号)、SID(会话session ID)

调用getpid()函数可以获取当前进程的PID,该函数在/usr/include/unistd.h头文件中声明;

调用getppid()函数可以获取当前进程父进程的PID,该函数在/usr/include/unistd.h头文件中声明;

调用getpgid(_pid_t _pid)函数可以获取指定进程_Pid的进程组号,该函数在/usr/include/unistd.h头文件中声明;

调用getsid(_pid_t _pid)函数可以获取指定进程_Pid的会话号SID(会话是一个或者多个进程的集合),该函数在/usr/include/unistd.h头文件中声明;

进程的用户属性:RUID(真实用户ID)、RGID(真是用户组ID)、EUID(有效用户ID)、EGID(有效用户组ID)

调用getuid()函数可以获取该进程的真实用户RUID(执行此程序的用户,创建该进程用户的UID为此进程的RUID),该函数在/usr/include/unistd.h头文件中声明;

调用geteuid()函数可以获取有效用户ID,该函数在/usr/include/unistd.h头文件中声明;

调用getgid()获取进程的用户组ID(创建进程的用户所在的组号),该函数在/usr/include/unistd.h头文件中声明;

调用getegid()获取有效进程用户组ID,该函数在/usr/include/unistd.h头文件中声明;

进程的管理

创建进程fork 和 vfork

在Linux环境下,创建进程的主要方法是调用fork函数。Linux下的所有进程都是由init进程(第一个进程PID=1)直接或者间接的创建,fork函数在/usr/include/unistd.h头文件中声明;

若fork函数调用,那么将产生两个进程分支(父进程和子进程),在父进程中返回子进程的ID(大于0),在子进程中返回0;若调用失败,在父进程中返回0;

下面是创建调用fork函数的程序

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

int main()
{
        pid_t pid;

        if( (pid=fork()) == -1 ) //call fork() and test if succeed
        {
                printf("fork error!");
                return -1;
        }
        else if(pid == 0)  //in sub thread
        {
                printf("This is sub thread.\n");
                printf("in sub : My PID = %d\n",getpid());
                printf("in sub : My PPID = %d\n",getppid());
        }
        else   //in father thread
        {
                printf("This is father thread.\n");
                printf("in father : My PID = %d\n",getpid());
        }

        return 0;
}
子进程与父进程

子进程将复制父进程用户空间的所有信息(文件缓冲区、代码段、数据段、BSS段、堆、栈、文件描述符),其中复制的仅仅是文件描述符,但是对于与文件描述符关联的内核文件表项(struct file结构体)则是采用的是共享的方式。

下面的程序中,虽然子进程复制了父进程的文件描述符fd,但是和父进程操作的是同一个文件

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
    pid_t pid;
    int fd;
    int i=1;
    int status;
    char *ch1="hello";
    char *ch2="world";
    char *ch3="IN";
    if((fd=open("test.txt",O_RDWR|O_CREAT,0644))==-1)//打开一个文件
    {
        perror("parent open");
        exit(EXIT_FAILURE);
    }
    if(write(fd,ch1,strlen(ch1))==-1)//父进程向文件中写入字符串1
    {
        perror("parent write");
        exit(EXIT_FAILURE);
    }

    if((pid=fork())==-1)//创建一个子进程
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    else if(pid==0)  //在子进程中
    {
        i=2;
        printf("in child\n");
        printf("i=%d\n",i);
        if(write(fd,ch2,strlen(ch2))==-1)//子进程复制了父进程的fd,然后向文件中写入字符串2
            perror("child write");
        return 0;
    }
    else
    {
        sleep(1);//休眠1秒钟,等待子进程执行完毕
        printf("in parent\n");
        printf("i=%d\n",i);
        if(write(fd,ch3,strlen(ch3))==-1)//父进程写入字符串3
            perror("parent,write");
        wait(&status);//等待子进程结束
        return 0;
    }
}
使用vfork创建进程

使用fork创建进程会复制父进程用户空间的大量数据,有些场景这样做是极大的浪费,所以使用vfork创建新进程时并不复制父进程的地址空间,而是在必要的时候才申请新的存储空间,vfork比fork可以极大程度的提高性能。vfork只在需要的时候复制,一般采用和父进程共享所有资源的方式处理。

下面的程序说明了vfork创建的子进程和父进程共享数据段和代码段。

#include<unistd.h>
#include<error.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
int glob=6;
int main()
{
        int var;
        pid_t pid;
        var=88;
        printf("in beginning:\tglob=%d\tvar=%d\n",glob,var);
        if((pid=vfork())<0)
        {
                perror("vfork");
                exit(EXIT_FAILURE);
        }
        else if(pid==0)
        {
                printf("in child,modify the var:glob++,var++\n");
                glob++;
                var++;
                printf("in child:\tglob=%d\tvar=%d\n",glob,var);
                _exit(0);
        }
        else
        {
                printf("in parent:\tglob=%d\tvar=%d\n",glob,var);
                return 0;
        }
}
上述程序共享glob和var变量,如果把上述中的vfork改为fork结果将会不一样。

结束进程

结束进程意味着要回收进程的资源,又可以分为回收进程用户空间的资源和回收进程内核空间的资源

回收用户进程空间的资源

进程在正常退出前都需要执行退出处理函数,刷新流缓冲区等操作,然后释放用户进程空间的所有资源,而进程在内核中的资源PCB并不会立即释放,仅仅调用退出函数而没有回收内核资源PCB的进程是一个僵死进程。

显示的调用exit或者_exit系统调用可以结束进程。调用exit将以反序的方式执行由on_exit函数和atexit函数注册的清理函数,同时刷新缓冲区,并把退出状态status返回给父进程。_exti是直接退出,仅仅把退出状态交给父进程,不会进行清理工作和刷新缓冲区。

注意:exit和return的区别,exit退出当前进程,而return仅仅退出当前函数。

回收内核空间的资源

回收内核空间的资源不是由退出进程本身完成的,而是由该进程的父亲进程完成的。调用wait函数的父进程将阻塞式的等待该进程的任意一个子进程结束后,回收该子进程的内核空间的资源,wait函数定义在/usr/include/sys/wait.h头文件中。还可以使用waitpid函数等待指定子进程结束,其定义在/usr/include/sys/Wait.h中

孤儿进程和僵死进程

孤儿进程:因为父进程退出而导致一个子进程被init进程收养的进程称为孤儿进程,即孤儿进程的父进程改为init进程。

僵死进程:进程已经退出(用户空间的资源已经被回收),但是其父亲进程还没有回收内核资源的进程称为僵死进程,即该进程在内核空间的PCB没有被释放。

下面的程序演示了一个孤儿进程

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

int main()
{
        pid_t pid;
        if((pid=fork())==-1)
                perror("fork");
        else if(pid==0)
        {
                printf("sub : pid=%d,ppid=%d\n",getpid(),getppid());
                sleep(2);
                printf("sub : pid=%d,ppid=%d\n",getpid(),getppid());
        }
        else
        {
                printf("father : pid=%d,ppid=%d\n",getpid(),getppid());
                sleep(1);
                exit(0);
        }
}
下面的程序演示了一个僵死进程  
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
        pid_t pid;
        if((pid=fork())==-1)
                perror("fork");
        else if(pid==0)
        {
                printf("child_pid pid=%d\n",getpid());
                exit(0);
        }
        sleep(3);
        system("ps");
        exit(0);
}
程序的运行结果:  Linux进程与子进程

运行结果中的PID=10375的进程僵死,如果此时打开/proc/10375/maps(该进程的用户进程空间信息),可以发现为空,也就是用户进程空间资源已经被回收。

守护进程

守护进程是一种在后台运行的特殊进程,它脱离终端,从而可以避免守护进程被任何终端信号打断,它执行产生的信息也不在任何终端显示。守护进程周期性的执行某种任务或等待处理某些发生的事件,Linux的大多数服务器就是使用守护进程实现的,比如web服务器httpd等。

一般情况下,守护进程可以通过以下方式启动:

(1)在系统启动时启动脚本,通常放在/etc/rc.d目录中;

(2)利用inet超级服务器启动,如telnet等;

(3)由cron命令定时启动以及在终端用nohup命令启动的进程也是守护进程。

创建一个守护进程需要遵循以下要点:

(1)屏蔽一个有关控制终端操作的信号

(2)在后台运行。方法是在进程中创建子进程,并使父进程终止,使其在子进程中后台运行。

(3)脱离终端控制和进程组,这是因为一个进程属于一个进程组,同进程组的进程共享一个控制终端,而一个进程关联的控制终端和进程组通常是从父进程继承下来的,因此子进程仍然受到父进程终端的影响。可以使用setsid()使子进程成为一个新的会话组长和进程组长,与原来的进程组脱离关系。

(4)禁止进程重新打开控制终端。进程已经成为一个无终端的会话组长,但是仍然有权限申请一个终端,只有会话组长才能打开终端。采用的办法是再创建一个子进程,并让父进程退出,该子进程就不再是会话组长了,而且父进程也没有终端控制。

(5)关闭打开的文件描述符。子进程会从父进程复制文件描述符,这对守护进程没有意义,反而会浪费系统资源,造成进程所占的文件系统无法卸载。

(6)改变当前工作目录。进程活动的时候,进程工作目录所在的文件系统不能卸载,因此一般将守护进程的工作目录设置为合适的目录。

(7)重设文件创建的掩码。进程从父进程那里继承文件创建掩码umask,它可能修改守护进程所创建的文件的存储权限,一般将文件创建掩码清除。

(8)处理SIGCHLD信号(子进程退出信号)