02.进程的基础

1.进程的概念

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,

是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

重点:

  • 系统进行资源分配的基本单位
  • 进程是程序的基本执行实体(包含线程)
  • 进程是线程的容器
  • 程序是指令、数据及其组织形式的描述,进程是程序的实体
  • 进程是程序的一次执行--动态的
  • 程序包含了所需要实现功能的代码,就是一个文件--静态的
  • 不同的进程之间是相互独立的,指的是存储空间是独立的

进程是60年代初首先由麻省理工学院的MULTICS系统和IBM公司的CTSS/360系统引入的。 [2]

进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。

进程中包含:文本区域(text region)、数据区域(data region)和堆栈(stack region)

以上进程中包含的内容通过什么样的形式表现出来的--进程控制块

多道程序(了解)

多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。与之相对应的是单道程序,即在计算机内存中只允许一个的程序运行。

对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个。

操作系统引入进程的概念的原因:

从理论角度看,是对正在运行的程序过程的抽象;

从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。

进程组成:程序 数据 进程控制块

进程的创建数量:

cat /proc/sys/kernel/pid_max

进程的状态:

就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间。

执行态:该进程正在占用 CPU 运行。

等待态:进程因不具备某些执行条件而暂时无法继续执行的状态。

进程控制块

位置:

/usr/src/linux-headers-5.4.0-84-generic/include/linux/sched.h

进程控制块中包含的内容:

调度数据:

进程的状态、标志、优先级、调度策略等。

时间数据:

创建该进程的时间、在用户态的运行时间、在内核态的运行时间等。

文件系统数据:

umask 掩码、文件描述符表等。内存数据、进程上下文、进程标识(进程号)

struct task_struct {
    /*
     * offsets of these are hardcoded elsewhere - touch with care
     */
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */ //进程当前的状态
    unsigned long flags;    /* per process flags, defined below */    //反应进程状态的信息,但不是运行状态,定义见下
    int sigpending; //进程收到了信号,但尚未处理
    mm_segment_t addr_limit;    /* thread address space: //虚存地址上限
                         0-0xBFFFFFFF for user-thead
                        0-0xFFFFFFFF for kernel-thread
                     */
    struct exec_domain *exec_domain;
    volatile long need_resched;    //与进程调度有关表示用户从系统空间按返回用户空间要执行的一次调度
    unsigned long ptrace;
 
    int lock_depth;        /* Lock depth */
 
/*
 * offset 32 begins here on 32-bit platforms. We keep
 * all fields in a single cacheline that are needed for
 * the goodness() loop in schedule().
 */
    long counter; //与进程调度相关
    long nice;
    unsigned long policy;    //实用于本进程的调度政策
    struct mm_struct *mm;
    int processor;
    /*
     * cpus_runnable is ~0 if the process is not running on any
     * CPU. It's (1 << cpu) if it's running on a CPU. This mask
     * is updated under the runqueue lock.
     *
     * To determine whether a process might run on a CPU, this
     * mask is AND-ed with cpus_allowed.
     */
    unsigned long cpus_runnable, cpus_allowed;
    /*
     * (only the 'next' pointer fits into the cacheline, but
     * that's just fine.)
     */
    struct list_head run_list;
    unsigned long sleep_time;
 
    struct task_struct *next_task, *prev_task; //内核会对每一个进程做点什么事情的时候,常常需要将其连成一个队列,这2个指针用于这个目的
    struct mm_struct *active_mm;
    struct list_head local_pages;
    unsigned int allocation_order, nr_local_pages;
 
/* task state */
    struct linux_binfmt *binfmt;//应用文件格式
    int exit_code, exit_signal;
    int pdeath_signal;  /*  The signal sent when the parent dies  */
    /* ??? */
    unsigned long personality; //进程的个性化信息,详细见下
    int did_exec:1;
    unsigned task_dumpable:1;
    pid_t pid; //进程号
    pid_t pgrp;
    pid_t tty_old_pgrp;
    pid_t session;
    pid_t tgid;
    /* boolean value for session group leader */
    int leader;
    /* 
     * pointers to (original) parent process, youngest child, younger sibling,
     * older sibling, respectively.  (p->father can be replaced with 
     * p->p_pptr->pid)
     */
    struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; //用于族谱信息的,例如p_opptr指向父进程
    struct list_head thread_group;
 
    /* PID hash table linkage. */
    struct task_struct *pidhash_next;
    struct task_struct **pidhash_pprev; //pid是随机分配的,我们常常使用kill pid想进程发送信号(大部分人认为是杀死进程,其实这是个发送信号的指令,默认的参数为杀死。如果想暂停某进程,只需kill STOP 进程的PID),这里可以看到根据pid寻找进程的操作是经常被使用的,而pid又是随机分配,于是这里边用这2个指针指向一个杂凑数组,数组是按照杂凑的算法,以pid为关键字建立,方便根据pid来寻找task_struct
 
    wait_queue_head_t wait_chldexit;    /* for wait4() */
    struct completion *vfork_done;        /* for vfork() */
    unsigned long rt_priority;    //优先级
    unsigned long it_real_value, it_prof_value, it_virt_value;
    unsigned long it_real_incr, it_prof_incr, it_virt_incr;
    struct timer_list real_timer;
    struct tms times; //运行时间的总汇
    unsigned long start_time;
    long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; //在多个处理器上运行于系统空间和用户空间的时间
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
    unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;//发生页面异常的次数和换入换出的次数
    int swappable:1;
/* process credentials */
    uid_t uid,euid,suid,fsuid;
    gid_t gid,egid,sgid,fsgid; //与文件权限有关的
    int ngroups;
    gid_t    groups[NGROUPS];
    kernel_cap_t   cap_effective, cap_inheritable, cap_permitted; //权限,比如该进程是否有权限从新引导系统,这里是大概介绍
    int keep_capabilities:1;
    struct user_struct *user;    //指向该进程拥有的用户
/* limits */
    struct rlimit rlim[RLIM_NLIMITS]; //进程对各种资源使用数量的限制,详细见下
    unsigned short used_math;
    char comm[16];
/* file system info */
    int link_count, total_link_count;
    struct tty_struct *tty; /* NULL if no tty */
    unsigned int locks; /* How many file locks are being held */
/* ipc stuff */
    struct sem_undo *semundo;
    struct sem_queue *semsleeping;
/* CPU-specific state of this task */
    struct thread_struct thread;
/* filesystem information */
    struct fs_struct *fs;
/* open file information */
    struct files_struct *files;
/* namespace */
    struct namespace *namespace;
/* signal handlers */
    spinlock_t sigmask_lock;    /* Protects signal and blocked */
    struct signal_struct *sig;
 
    sigset_t blocked;
    struct sigpending pending;
 
    unsigned long sas_ss_sp;
    size_t sas_ss_size;
    int (*notifier)(void *priv);
    void *notifier_data;
    sigset_t *notifier_mask;
    
/* Thread group tracking */
       u32 parent_exec_id;
       u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty */
    spinlock_t alloc_lock;
 
/* journalling filesystem info */
    void *journal_info;
};
 
#define TASK_RUNNING        0 //不是表示正在运行,而是表示可以被调用
#define TASK_INTERRUPTIBLE    1
#define TASK_UNINTERRUPTIBLE    2
#define TASK_ZOMBIE        4
#define TASK_STOPPED        8 //对应于task_struct中的state,进程运行状态
 
 
//对应task_struct的flag
#define PF_ALIGNWARN    0x00000001    /* Print alignment warning msgs */
                    /* Not implemented yet, only for 486*/
#define PF_STARTING    0x00000002    /* being created */
#define PF_EXITING    0x00000004    /* getting shut down */
#define PF_FORKNOEXEC    0x00000040    /* forked but didn't exec */
#define PF_SUPERPRIV    0x00000100    /* used super-user privileges */
#define PF_DUMPCORE    0x00000200    /* dumped core */
#define PF_SIGNALED    0x00000400    /* killed by a signal */
#define PF_MEMALLOC    0x00000800    /* Allocating memory */
#define PF_MEMDIE      0x00001000       /* Killed for out-of-memory */
#define PF_FREE_PAGES    0x00002000    /* per process page freeing */
#define PF_NOIO        0x00004000    /* avoid generating further I/O */
#define PF_FSTRANS    0x00008000    /* inside a filesystem transaction */
 
#define PF_USEDFPU    0x00100000    /* task used FPU this quantum (SMP) */
 
//进程的个性化信息
enum {
    MMAP_PAGE_ZERO =    0x0100000,
    ADDR_LIMIT_32BIT =    0x0800000,
    SHORT_INODE =        0x1000000,
    WHOLE_SECONDS =        0x2000000,
    STICKY_TIMEOUTS    =    0x4000000,
    ADDR_LIMIT_3GB =    0x8000000,
};
 
/*
 * Personality types.
 *
 * These go in the low byte.  Avoid using the top bit, it will
 * conflict with error returns.
 */
enum {
    PER_LINUX =        0x0000,
    PER_LINUX_32BIT =    0x0000 | ADDR_LIMIT_32BIT,
    PER_SVR4 =        0x0001 | STICKY_TIMEOUTS | MMAP_PAGE_ZERO,
    PER_SVR3 =        0x0002 | STICKY_TIMEOUTS | SHORT_INODE,
    PER_SCOSVR3 =        0x0003 | STICKY_TIMEOUTS |
                     WHOLE_SECONDS | SHORT_INODE,
    PER_OSR5 =        0x0003 | STICKY_TIMEOUTS | WHOLE_SECONDS,
    PER_WYSEV386 =        0x0004 | STICKY_TIMEOUTS | SHORT_INODE,
    PER_ISCR4 =        0x0005 | STICKY_TIMEOUTS,
    PER_BSD =        0x0006,
    PER_SUNOS =        0x0006 | STICKY_TIMEOUTS,
    PER_XENIX =        0x0007 | STICKY_TIMEOUTS | SHORT_INODE,
    PER_LINUX32 =        0x0008,
    PER_LINUX32_3GB =    0x0008 | ADDR_LIMIT_3GB,
    PER_IRIX32 =        0x0009 | STICKY_TIMEOUTS,/* IRIX5 32-bit */
    PER_IRIXN32 =        0x000a | STICKY_TIMEOUTS,/* IRIX6 new 32-bit */
    PER_IRIX64 =        0x000b | STICKY_TIMEOUTS,/* IRIX6 64-bit */
    PER_RISCOS =        0x000c,
    PER_SOLARIS =        0x000d | STICKY_TIMEOUTS,
    PER_UW7 =        0x000e | STICKY_TIMEOUTS | MMAP_PAGE_ZERO,
    PER_HPUX =        0x000f,
    PER_OSF4 =        0x0010,             /* OSF/1 v4 */
    PER_MASK =        0x00ff,
};
 
//进程资源的限制,对应task_struct中的struct rlimit rlim[RLIM_NLIMITS],RLIM_NLIMITS的值是11,代表11项资源,分别是
#define RLIMIT_CPU    0        /* CPU time in ms */
#define RLIMIT_FSIZE    1        /* Maximum filesize */
#define RLIMIT_DATA    2        /* max data size */
#define RLIMIT_STACK    3        /* max stack size */
#define RLIMIT_CORE    4        /* max core file size */
#define RLIMIT_RSS    5        /* max resident set size */
#define RLIMIT_NPROC    6        /* max number of processes */
#define RLIMIT_NOFILE    7        /* max number of open files */
#define RLIMIT_MEMLOCK    8        /* max locked-in-memory address space */
#define RLIMIT_AS    9        /* address space limit */
#define RLIMIT_LOCKS    10        /* maximum file locks held */

进程相关的几个概念

进程号:用于区分不同的进程,类似于ID号

每个进程都由一个进程号来标识,其类型为 pid_t,进程号的范围: 0~ 32767。

进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使

用了。

在 linux 系统中进程号由 0 开始。

进程号为 0 及 1 的进程由内核创建。

进程号为 0 的进程通常是调度进程,常被称为交换进程(swapper)。进程号为 1

的进程通常是 init 进程。

除调度进程外,在 linux 下面所有的进程都由进程 init 进程直接或者间接创建。

进程号(PID)

标识进程的一个非负整型数。

父进程号(PPID)

任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程

的父进程,对应的进程号称为父进程号(PPID)。

进程组号(PGID)

进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。

进程号相关的几个函数:

Linux 操作系统提供了三个获得进程号的函数 getpid()、 getppid()、 getpgid()

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void)
功能:获取本进程号(PID)
pid_t getppid(void)
功能:获取调用此函数的进程的父进程号(PPID)
pid_t getpgid(pid_t pid)
功能:获取进程组号(PGID),参数为 0 时返回当前 PGID,否则返回参数指定的进程的
PGID
#include <stdio.h>
#include <sys/types.h>  //open
#include <sys/stat.h>  //open
#include <fcntl.h>     //open 
#include <unistd.h> //read  write  close
#include <errno.h>  //perror

int main(void)
{
    //getpid()、 getppid()、 getpgid()
    pid_t pid_n=0;
    pid_t pid_c=0;
    pid_c=getpid();
    printf("当前进程的ID号:%d\n",pid_c);

    pid_n=getppid();
    printf("当前进程的父进程的ID号:%d\n",pid_n);

    pid_n=getpgid(pid_c);
    printf("当前进程的组ID号:%d\n",pid_n);
    return 0;
}

fork函数

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);//我们不是用
创建一个新进程
pid_t fork(void)
功能:
fork()函数用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进
程称为父进程。
返回值:
成功:子进程中返回 0,父进程中返回子进程 ID。
失败:返回-1。

使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。

地址空间:

包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等--PCB进程控制块中。

子进程所独有的只有它的进程号,计时器等。因此,使用 fork 函数的代价是很大的。

#include <stdio.h>
#include <sys/types.h>  //open
#include <sys/stat.h>  //open
#include <fcntl.h>     //open 
#include <unistd.h> //read  write  close
#include <errno.h>  //perror

int main(void)
{
    pid_t pid;
    pid=fork();//调用一次执行两次返回三个结果
    
    if(pid==0)//子进程
    {
        while(1)
        {
            printf("当前进入子进程\n");
            sleep(1);
        }        
    }
    else if(pid>0)
    {
        while(1)
        {
            printf("当前进入父进程\n");
            sleep(1);
        }
    }
    return 0;
}

过结果可得,父进程和子进程的执行是没有先后顺序的,随机的,无法判断的

具体的话需要去看内核中的调度算法

子进程和父进程是相互独立的:

#include <stdio.h>
#include <sys/types.h>  //open
#include <sys/stat.h>  //open
#include <fcntl.h>     //open 
#include <unistd.h> //read  write  close
#include <errno.h>  //perror

int val=10;
int main(void)
{
    int data=20;
    pid_t pid;
    pid=fork();
    if(pid==0)//子进程
    {
        while(1)
        {
            
            printf("当前进入子进程\n");
            sleep(1);
            printf("val=%d\n data=%d\n",val,data);
        }
        
    }
    else if(pid>0)//父进程
    {
        while(1)
        {
            val++;
            data++;
            printf("当前进入父进程\n");
            sleep(1);
            printf("father_val=%d\n father_data=%d\n",val,data);
        }
    }
    return 0;
}

fork函数的作用

vfork函数

pid_t vfork(void)
功能:
vfork 函数和 fork 函数一样都是在已有的进程中创建一个新的进程,但它们创
建的子进程是有区别的。
返回值:
创建子进程成功,则在子进程中返回 0,父进程中返回子进程 ID。出错则返回-1。

vfork函数和fork函数的区别

fork()函数和vfork()函数都是用于创建新进程的系统调用函数,它们之间有以下区别:

内存管理:

  • fork()函数会创建一个子进程,子进程将复制父进程的完整内存空间,包括代码段、数据段和堆栈等。子进程和父进程将拥有相同的内存副本,但是各自的内存是独立的,不会相互影响。
  • vfork()函数创建的子进程与父进程共享内存空间。子进程会暂时使用父进程的地址空间,直到调用exec()或者exit()函数。在这之前,子进程对内存的修改会直接影响到父进程。

执行顺序:

  • fork()函数调用后,父进程和子进程都会继续执行,但是执行顺序是不确定的,由操作系统的调度决定。
  • vfork()函数调用后,子进程会先执行,而父进程会被阻塞,直到子进程调用exec()或者exit()函数。

效率:

  • fork()函数复制整个父进程的内存空间,涉及到页表的复制和写时复制(Copy-on-Write),因此相对较慢。
  • vfork()函数仅仅是共享父进程的内存空间,因此效率更高。

用途:

  • fork()函数通常用于创建一个完全独立的子进程,使父进程和子进程可以并行执行不同的任务。
  • vfork()函数一般用于创建一个临时的“子进程”,在该子进程调用exec()或者exit()之前,可以利用共享的地址空间来执行一些特定任务,比如调用一个新程序。
#include <stdio.h>
#include <stdlib.h>
 #include <sys/types.h>
 #include <dirent.h>
 #include <string.h>
 #include <unistd.h>

int main(int argv,char *argc[])
{
    int data=20;
    int i=0;
    pid_t pid;
    pid=vfork();
    if(pid==0)//子进程
    {
        for(i=0;i<3;i++)
        {
            
            printf("当前进入子进程\n");
            sleep(1);
            // printf("val=%d\n data=%d\n",val,data);
        }
        exit(1);
    }

    else if(pid>0)//父进程
    {
        while(1)
        {
            printf("当前进入父进程\n");
            sleep(1);
           
        }
    }




    return 0;
}

查看进程-PS

Linux ps (英文全拼:process status)命令用于显示当前进程的状态,类似于windows 的任务管理器。
 
ps [options] [--help]
参数内容:
-A 列出所有的进程
-w 显示加宽可以显示较多的资讯
-au 显示较详细的资讯
-aux 显示所有包含其他使用者的进程


ps -aux|grep a.out //查找a.out进程 使用ps指令进行全局搜索,通过管道输出我们想要的a.out
|--管道
ps -aux
au(x) 输出格式 :
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
USER: 行程拥有者
PID: pid
%CPU: 占用的 CPU 使用率
%MEM: 占用的记忆体使用率
VSZ: 占用的虚拟记忆体大小
RSS: 占用的记忆体大小
TTY: 终端的次要装置号码 (minor device number of tty)
STAT: 该行程的状态:
    D: 无法中断的休眠状态 (通常 IO 的进程)
    R: 正在执行中
    S: 静止状态
    T: 暂停执行
    Z: 不存在但暂时无法消除
    W: 没有足够的记忆体分页可分配
    <: 高优先序的行程
    N: 低优先序的行程
    L: 有记忆体分页分配并锁在记忆体内 (实时系统或捱A I/O)
START: 行程开始时间
TIME: 执行的时间
COMMAND:所执行的指令

进程的挂起和等待

父子进程有时需要简单的进程间同步,如父进程等待子进程的结束。

linux 下提供了以下两个等待函数 wait()、 waitpid()。

 #include <sys/types.h>
 #include <sys/wait.h>

 pid_t wait(int *wstatus); //阻塞性
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
调用 wait 函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽
视的信号时才被唤醒。
若调用进程没有子进程或它的子进程已经结束,该函数立即返回。  
参数:
函数返回时,参数 status 中包含子进程退出时的状态信息。子进程的退出信息在
一个 int 中包含了多个字段,用宏定义可以取出其中的每个字段。
取出子进程的退出信息
    WIFEXITED(status)
如果子进程是正常终止的,取出的字段值非零。
    WEXITSTATUS(status)
返回子进程的退出状态,退出状态保存在 status 变量的 8~16 位。在用此宏前
应先用宏 WIFEXITED 判断子进程是否正常退出,正常退出才可以使用此宏。
 
返回值:
如果执行成功则返回子进程的进程号。
出错返回-1,失败原因存于 errno 中。 
                                                                                                                                                                                                       
 pid_t waitpid(pid_t pid, int *wstatus, int options);
 功能:  
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
返回值:
如果执行成功则返回子进程 ID。
出错返回-1,失败原因存于 errno 中。    

参数 pid 的值有以下几种类型:
pid>0:
等待进程 ID 等于 pid 的子进程。
pid=0
等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,
waitpid 不会等待它。
pid=-1:
等待任一子进程,此时 waitpid 和 wait 作用一样。
pid<-1:
等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
status 参数中包含子进程退出时的状态信息。
options 参数能进一步控制 waitpid 的操作:
0:
同 wait,阻塞父进程,等待子进程退出。
WNOHANG:
没有任何已经结束的子进程,则立即返回。
WUNTRACED
:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。
(跟踪调试,很少用到)  
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>  //open
#include <sys/stat.h>  //open
#include <fcntl.h>     //open 
#include <unistd.h> //read  write  close
#include <errno.h>  //perror
#include <sys/wait.h>


int val=10;
int main(void)
{
    int data=20;
    int count=0;
    pid_t pid;
    int sta=0;
    pid=fork();
    if(pid==0)//子进程
    {
        for(int i=0;i<3;i++)
        {
            count++;

            printf("当前进入子进程--%d\n",count);
            sleep(1);
        } 
        exit(1);
        
    }
    else if(pid>0)//父进程
    {
         wait(&sta);
         if(WIFEXITED(sta)!=0)  
         {
            printf("当前执行的子进程已经退出%d\n",WEXITSTATUS(sta));
         }
         printf("父进程即将在三秒后退出......\n");
            sleep(3);
           
       
    }
    return 0;
}

waitpid--非阻塞

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>  //open
#include <sys/stat.h>  //open
#include <fcntl.h>     //open 
#include <unistd.h> //read  write  close
#include <errno.h>  //perror
#include <sys/wait.h>


int val=10;
int main(void)
{
    int data=20;
    int count=0;
    pid_t pid;
    int sta=0;
    pid=fork();
    if(pid==0)//子进程
    {
        for(int i=0;i<3;i++)
        {
            count++;
            printf("当前进入子进程--%d\n",count);
            sleep(1);
        } 
        exit(1);
        
    }
    else if(pid>0)//父进程
    {
      //  waitpid(pid,&sta,0);//阻塞性
        waitpid(0,&sta,WNOHANG);//非阻塞性
         printf("父进程即将在三秒后退出......\n");
        sleep(3);           
    }
    return 0;
}

waitpid(0,&sta,WNOHANG);

waitpid(pid,&sta,0);

僵尸进程和孤儿进程

僵尸进程:(危害较大)

僵尸进程是指子进程已经终止,但是父进程尚未处理其终止状态的进程。僵尸进程在操作系统中存在一段时间,直到父进程通过调用适当的函数来获取子进程的终止状态。

僵尸进程的危害:

僵尸进程虽然不会直接导致系统崩溃,但它们可能会产生以下危害:

  • 系统资源浪费: 僵尸进程在操作系统中占用资源,包括进程表项、内存、文件描述符等。如果产生大量的僵尸进程,将占用宝贵的系统资源,导致系统性能下降。
  • 进程表溢出: 系统同时能够存在的进程数量是有限的。当产生过多的僵尸进程并且父进程没有处理它们时,进程表可能会耗尽,导致无法创建新的进程。
  • 子进程信息丢失: 僵尸进程的存在意味着父进程无法获取子进程的终止状态。父进程可能无法获知子进程是如何终止的,无法判断其是否以预期的方式退出,导致无法正确处理异常情况。
  • 安全问题: 如果僵尸进程的父进程异常终止或者其他原因导致父进程无法再处理僵尸进程,那么这些僵尸进程可能会被系统进程(如 init 进程)接管。这可能导致进程资源泄漏和系统安全问题。
  • init进程是我们的Ubuntu启动后运行的第一个进程--祖先进程

综上所述,虽然僵尸进程本身并不直接威胁系统稳定性,但它们会占用系统资源、导致进程表溢出,并可能导致子进程信息丢失和安全问题。因此,及时处理僵尸进程是保持系统健康运行的重要任务。

孤儿进程(危害相对较小)

孤儿进程是指其父进程已经终止或被其他进程接管而无法接收其终止状态的进程。在Unix-like操作系统中,孤儿进程会被特殊的init进程(进程ID为1)接管,init进程会定期检查并回收这些孤儿进程。

孤儿进程可能产生的主要原因有两种情况:

  • 当父进程比子进程更早地终止时,子进程可能会变成孤儿进程。
  • 父进程在子进程终止前未能调用wait() waitpid()

孤儿进程可能会导致以下问题:

  • 资源泄漏:孤儿进程占用系统资源,如进程表项、内存、打开的文件等,可能会导致资源浪费,特别是当大量孤儿进程存在时。
  • 进程状态无法正确追踪:由于其父进程无法获取孤儿进程的终止状态,可能会导致无法及时知道进程是如何终止的,无法正确处理异常情况。

为了避免孤儿进程的产生,父进程通常可以采取以下几种方式之一:

  • 正确地等待子进程终止:父进程可以使用wait()或waitpid()等类似函数,主动等待子进程终止并获取其终止状态,在子进程终止后及时处理。
  • 忽略SIGCHLD信号:通过忽略SIGCHLD信号,使父进程不接收到子进程终止的信号,这样子进程就成为孤儿进程,并由init进程接管和回收。

总之,孤儿进程可能导致资源浪费和进程状态追踪问题,但由于系统的处理机制,孤儿进程通常不会引发严重的问题。父进程可以通过适当的方式来避免或处理孤儿进程。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>  //open
#include <sys/stat.h>  //open
#include <fcntl.h>     //open 
#include <unistd.h> //read  write  close
#include <errno.h>  //perror
#include <sys/wait.h>


int val=10;
int sta=0;
int main(void)
{
    int data=20;
    pid_t pid;
    pid=fork();
    if(pid==0)//子进程
    {
        int count=0;
        while(1)
        {
            count++;
            printf("当前进入子进程\n");
            sleep(1);
            printf("val=%d\n data=%d\n",val,data);

            if(count>=5)
            {
                exit(1);
            }

        }
        
    }
    else if(pid>0)//父进程
    {
        while(1)
        {
            waitpid(0,&sta,WNOHANG);//非阻塞性
            val++;
            data++;
            printf("当前进入父进程\n");
            sleep(1);
            printf("father_val=%d\n father_data=%d\n",val,data);
        }
    }
    return 0;
}

守护进程

守护进程--后台进程

守护进程(daemon)是在后台运行的一种特殊类型的进程,它通常在系统启动时启动,并一直运行,不受用户登录和注销的影响。守护进程通常在操作系统中执行一些特定的任务或提供一些系统服务。

以下是一些守护进程的特点:

  • 在后台运行:守护进程在后台默默地运行,不与用户交互,并且通常没有控制终端。
  • 独立于登录会话:守护进程与用户登录和注销无关,它们独立于用户的操作而运行。

守护进程有父进程吗?守护进程是没有父进程

  • 没有标准输入输出:守护进程通常没有标准输入和输出,因为它们不与控制终端进行交互。
  • 长期运行:守护进程通常在系统启动时启动,并一直运行,直到系统关闭或主动停止。
  • 提供系统服务:守护进程通常提供一些系统服务,如网络服务(如HTTP服务器、FTP服务器)、日志服务、任务调度服务等。

创建守护进程的过程通常涉及以下步骤:

  • 分离控制终端:通过调用fork()创建子进程,然后在子进程中调用setsid(),将子进程从终端会话中分离出来。
  • 更改工作目录:通常会将守护进程的工作目录切换到根目录或其他适当的目录。
  • 重定向标准文件描述符:关闭或重定向标准输入、标准输出和标准错误文件描述符到/dev/null或其他适当的日志文件。
  • 执行守护进程任务:在守护进程中执行相应的任务,并保持长期运行。

守护进程的创建可以使用不同编程语言和操作系统接口来实现,具体的实现方式可能会有一些差异。一些常见的守护进程有sshd(SSH服务器守护进程)、httpd(HTTP服务器守护进程)等。

setsid
NAME
       setsid - creates a session and sets the process group ID
                 创建会话并设置进程组ID

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t setsid(void);
如果调用进程不是进程组的领导者,setsid()将创建一个新会话。呼叫过程是领导者使其会话ID与其
进程ID相同)。调用过程也成为会话中新流程组的流程组长(即,使其流程组ID与其流程相同

ID)。

调用进程将是新进程组和新会话中的唯一进程。最初,新会话没有控制终端。关于会话如何获取控制终端的细节,
请参阅凭据(7)。

返回值
成功后,将返回调用进程的(新)会话ID。出现错误时,返回(pid_t)-1,并设置errno以指示错误。
错误
EPERM任何进程的进程组ID等于调用进程的PID。

进程的终止

atexit()函数和exit()函数都是用于在程序退出时执行特定任务的函数,但它们之间有一些区别。

  • 调用顺序:atexit()函数允许注册多个退出处理函数,它们将按照注册的顺序逆向执行。而exit()函数只能执行一个退出处理函数,即使注册了多个。
  • 灵活性:atexit()函数比exit()函数更灵活,可以在程序的多个地方调用atexit()注册不同的退出处理函数,而exit()函数只能在程序的一个地方调用。
  • 返回值及退出码:atexit()函数没有返回值,而exit()函数会终止程序的执行并将退出码(通过参数传递给exit()函数)返回给操作系统,退出码可用于表示程序退出状态的信息。
  • 调用时机:atexit()函数注册的退出处理函数在exit()函数被调用时才会执行,而exit()函数本身是立即被调用的,它会终止程序的执行并立即执行退出处理函数。
  • 是否终止进程:atexit()函数不能直接终止进程的执行,而exit()函数会终止进程的执行并立即返回到操作系统。

进程的终止函数

void exit(int value);//属于库函数,进行了封装

void _exit(int value);//初拥系统调度中去使用的

补充部分:

终端部分:

#include <unistd.h>
char *ttyname(int fd);
参数:
fd是文件描述符,它表示一个打开的文件或者是标准输入、标准输出或标准错误的文件描述符。如果
fd是有效的且与终端设备相关联,ttyname()函数会返回一个指向终端设备文件名的指针。如果
fd不是终端设备或者无效的,ttyname()函数返回NULL。
作用:
用于获取指定文件描述符所关联的终端设备文件的路径名。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>  //open
#include <sys/stat.h>  //open
#include <fcntl.h>     //open 
#include <unistd.h> //read  write  close
#include <errno.h>  //perror
#include <sys/wait.h>
int sta=0;
int val;
int main(void)
{
   
    pid_t pid;
   
    pid=fork();
    if(pid==0)//子进程
    {
       printf("子进程执行成功\n");
       scanf("%d",&val);
       printf("子进程-->当前终端为:%s-->data=%d-->pid=%d\n",ttyname(0),val,getpid());
       // printf("父进程pid=%d\n",ttyname(0),val,getpppid());
       exit(1);
    }
    else if(pid>0)//父进程
    {
        // waitpid(pid,&sta,0);//阻塞性
        printf("父进程正在运行\n");
        scanf("%d",&val);
        printf("父进程-->当前终端为:%s-->data=%d-->pid=%d\n",ttyname(0),val,getpid());
        
        
    }
    return 0;
}

进程组和会话:

进程组和会话都是用于管理和组织进程的概念,但它们在层级和作用上有所区别。下面列出了它们的主要区别:

层级关系:

  • 进程组:是操作系统中一组相关进程的集合,这些进程拥有相同的进程组ID。进程组使得可以同时向组内的所有进程发送信号(例如中断或终止信号)。这对于控制终端中运行的命令和它们衍生的子进程特别有用。
  • 会话:是一个或多个进程组的集合,这些进程共享相同的会话ID,会话本质上代表一个用户的登录实例。会话控制了终端和进程之间的交互,并且通常在用户登陆时开始,在用户注销时结束。

会话:当前某一个用户登录所能控制的所有资源集合

进程的关联性:

  • 进程在创建时会加入创建它的进程的组(除非特别指定加入另一个组),同一个进程组内的进程通常是协同工作,或是具有相同行为特征的一组进程。
  • 会话可以包含多个进程组。一个会话开始于一个登录shell或用户启动的会话领导者进程,并且可能包含多个协作的进程组,这些进程组可以独立进行任务。

前台和后台控制:

  • 进程组:一个会话中可以有多个进程组,但在任何时刻只有一个进程组可以是前台进程组(可接收来自终端的输入),其他的都是后台进程组。
  • 会话:管理前台和后台进程组,可以控制终端访问(例如,只有前台进程组可以从终端读取输入)。

生命周期:

  • 进程组的生命周期通常与组内进程的创建和结束相关,特定任务完成或用户中断可能导致进程组的解散。
  • 会话的生命周期通常与用户会话相关。当用户登录时会话开始,当用户注销或会话领导者进程结束时会话结束。

终端控制权:

  • 会话与控制终端(controlling terminal)相关联。会话中的前台进程组可以接收终端的输入并处理终端的输出。
  • 进程组并不直接与控制终端相关联,控制终端的概念在会话层级中被管理。

简而言之,进程组是组织相关进程的一种方式,而会话是管理用户登录及其所启动进程组的更高级抽象。会话可以控制前台和后台进程组,管理与控制终端的交互,并在用户登陆和注销时开始和结束。

进程组和会话的概念在多进程编程和作业控制等领域有着广泛的应用,可以通过管理进程组和会话来实现对进程的控制和协调。

进程组:本质就是多个进程的集合

会话:指的就是多个进程组的集合

PS ajx---查看进程组和会话

PPID--父进程ID

PID--当前进程

PGID--组ID

SID--会话

进程组框架:

会话的创建步骤:

1) 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2) 该调用进程是组长进程,则出错返回
3) 该进程成为一个新进程组的组长进程
4) 需有 root 权限(ubuntu 不需要)
5) 新会话丢弃原有的控制终端,该会话没有控制终端
6) 建立新会话时,先调用 fork, 父进程终止,子进程调用 setsid

设置会话的函数就是setsid函数。
获取会话:pid_t getsid(pid_t pid);

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值