Linux C学习笔记——进程控制

来源:互联网 发布:玩守望先锋网络不稳定 编辑:程序博客网 时间:2024/06/05 07:50

本文为清华大学出版社出版的《Linux C从入门到精通》一书的学习笔记,在此感谢作者及相关人员。本文为原创学习笔记,转载请注明出处,谢谢。


7.1 进程概述

7.1.1 进程的定义

1)进程的定义

①进程是一个具有独立功能的程序关于某个数据集合的一次运行活动

②进程是一个程序与其数据一道通过处理机的执行所发生的活动

③进程是一个“执行中的程序”,即程序在处理机上执行时所发生的活动,而程序只是行为的一种规则

2)进程的特性

动态性 并发性 独立性 异步性 结构特性

7.1.2 进程的相关信息

1)进程ID:在Linux系统中,每一个进程都有其唯一的ID。在Linux系统下编写关于进程的C程序时,经常用到这样一个数据类型pid_t,该数据类型专门用来定义进程ID,其实可以将这个数据类型理解为一个非负数整数。

2)进程的状态:进程有3中基本状态,分别是运行状态、等待状态和结束状态。除了这三种基本状态外,进程还有就绪、挂起和僵尸等状态。

3)进程切换:关于进程切换,就是从正在运行的进程中收回处理器的使用权,等待运行进程进来时占用此处理器。

4)虚拟内存:在Linux系统中,每个进程都运行在格子的虚拟内存空间中。在Linux系统中的虚拟内存具有以下几点功能,如拥有巨大的寻址空间、可以共享虚拟内存及对进程进行保护等。

7.2 进程的基本操作

7.2.1 进程创建

1)fork()函数:fork()函数的功能是创建一个新的进程,新进程为当前进程的子进程,那么当前进程就被称为父进程。在一个函数中,可以通过fork()函数的返回值判断进程是在子进程中还是在父进程中。调用形式为:pid_t fork(void)

    使用fork()函数需要引用<sys/types.h>和<unistd.h>头文件,该函数的返回值类型为pid_t,表示一个非负整数。若程序运行在父进程中,函数返回的PID子进程的进程号;若程序运行在紫禁城中,返回PID为0。

    如若调用fork()函数创建子进程失败,那么就会返回-1,并且提示错误信息。错误信息有以下两种形式:

    EAGAIN:表示fork()函数没有足够的内存用于赋值父进程的分页表和进程结构数据。

    ENOMEN:表示fork()函数分配必要的内核数据结构时,内存不足。

2)vfork()函数:vfork()函数与fork()函数相同,都是系统调用函数,两者的区别是在创建子进程时fork()函数会复制所有的父进程的资源,包括进程环境、内存资源等,而vfork()函数在创建子进程时不会复制父进程的所有资源,父进程共享地址控件。      这样,子进程中对虚拟内存控件变量的修改,实际上是在修改父进程虚拟内存空间的值。

     在使用vfork()函数时,父进程会被阻塞,需要在子进程中调用_exit()函数退出子进程,不能使用exit()退出函数。

3)exec()函数族:通过调用fork()函数和vfork()函数创建子进程,子进程和父进程执行的代码是相同的。但是,通常创建了一个新进程也就是子进程后,目的是要执行与父进程不同的操作,实现不同的功能。因此Linux系统提供了一个exec()函数族,用      于创建和修改子进程。调用exec()函数时,子进程中的代码段、数据段、和堆栈段都将被替换。由于调用exec()函数并没有创建新进程,因此修改后的子进程ID并没有改变。exec()函数族由6种以exec()开头的函数组成,定义形式分别如下:

  int execl(const char *path, const char *arg, ...)

  int execlp(honst char *file, const char *arg, ...)

  int execle(const char *pathconst char *arg, ..., char* const envp[])

  int execv(const char *path, const char *argv[])

  int execve(const char *path, const char *argv[], char *const envp[])

  int execvp(const char *file, const *argv[])

  这些函数都定义在系统函数库中,在使用前需要引用头文件<sys/types.h>和<unistd.h>,并且必须在预定义时定义一个外部全局变量,例如:extern char **environ;

  上面定义的变量是一个指向Linux系统全局变量的指针。定义了这个变量后,就可以在当前工作目录中执行系统程序,如同在shell中不输入路径直接运行VIM和Emacs等程序一样。

  exec()函数族中的函数都实现了对子进程中的数据段、代码段和堆栈段进行替换的功能,如果调用成功,则加载新的程序,没有返回值。如果调用出错,则返回值为-1.

  这几个exec()函数的书写方式很相似,很容易记混,但是这几个函数都各有区别。通过exec()函数名称的拼写规律可以轻松帮助读者牢记这几个函数实现替换功能的不同方法。

  ①函数名中带有字母p:字母p是path的首字母,代表文件的绝对路径(或称相对路径)。当前函数名中带有字符p时,函数的参数就可以不用写出文件的相对路径,只写出文件名即可,因为函数会自动搜索系统的path路径

  ②函数名中带有字符l:字符l是list的首字母,表示需要将新程序的每个命令行参数都当做一个参数传给它,参数个数是可变的,并且最后要求输入一个NULL参数,表示参数输入结束。

  ③函数名中带有字母v:字符v是vector的首字母,表示该类函数支持使用参数数组,数组中的最后一个指针也要输入NULL参数,作为结束标志,这个参数数组就类似于main()函数的形式argv[]

  ④函数名以e结尾:字符e是environment的首字母,该类函数表示可以将一份新的环境变量表传给它。

  在exec()函数族中,execve()函数是其余5个exec()函数的基础,因为只有execve()函数时经过系统调用的,其余5个函数在执行时,都要在最后调用一次execve()函数。

7.2.2 进程等待

1)进程等待就是为了同步父进程和子进程,通常需要通过调用wait()等待函数使父进程等待子进程结束。如果父进程没有调用等待函数,子进程就会进入“僵尸(Zombie)”状态。了解了等待函数的工作过程,就可以知道为什么蜜柚调用等待函数时,子进程会进入僵尸状态,关于进入进程的等待状态,Linux系统提供的等待函数原型如下:

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int *status)

pid_t waitpid(pid_t pid, int status, int options);

int waitid(idtype_t idtype, id_t id, siginfo_t *info, int options)

2)wait()函数系统调用的工作过程是:首先判断子进程是否存在,即是否成功创建了一个子进程。如果创建失败,子进程不存在,则会直接退出进程,并且提示相关粗无信息;如果创建成功,那么wait()函数会将父进程挂起,直到子进程结束,并且返回结束时的状态和最后结束的子进程的PID。如果不存在子进程,提示的错误信息为ECHILD,表示wait()系统调用的进程没有可以等待的子进程。如果存在子进程,退出晋城市的结束状态可能有两种:

①子进程正常结束:当调用wait()函数,子进程正常结束后,函数会返回子进程PID和status状态,此时的参数status所指向的状态变量就存放在紫禁城的退出码中。退出码是所谓的从子进程的main()函数中返回的值或者子进程中exit()函数的参数。

②信号引起子进程结束:wait()函数系统调用中发送信号给子进程,可能会导致子进程结束运行。若发送的信号被子进程捕获,就会起到终止子进程的作用;若信号没有被子进程捕获,则会使子进程非正常结束。此时参数status返回的状态值为接收到的信号值,存放在最后一个字节中。

7.2.3 进程结束

当想要终止或者结束一个进程时,会使用系统调用exit()函数正常退出进程。该系统调用包括exit()和_exit()两个函数。

#include<stdlib.h>

void exit(int status)

#include<unistd.h>

void _exit(int status)

exit()函数的作用是终止进程,并将运算status & 0377 表达式后的值返回给父进程,在父进程中可以通过wait()函数获得该值。

_exit()函数与exit()函数相同,无论成功与否,都没有返回信息。

在前面讲述进程创建时,强调了vfork()函数创建的子进程在退出时只能用_exit()函数退出进程,而不能使用exit()函数退出进程,这就是两个函数的区别造成的。在调用eixt()函数时,会对输入/输出流进行刷新,释放所占用的资源以及清空缓存区等;而_exit()函数则不具备刷新缓冲区等操作的功能

在exit系统调用中,函数exit()在终止进程时会关闭所有文件,清空缓冲区。因此,如果在fork()函数和vfork()函数中使用exit()函数终止子进程,会清空标准输入/输出流,可能造成临时文件丢失,并且vfork()函数时父子进程共享虚拟内存,如果在子进程中使用exit()函数会严重影响到父进程,所以在使用这两个创建进程的函数时,尽量都不要使用exit()函数终止子进程。

7.3 多个进程间的关系

7.3.1 进程组

所谓进程组,就是一个或多个进程的集合。作为一个进程组,里面的每一个进程都有统一的进程标识。在Linux系统中,可以通过getpgrp()函数获取进程组ID,该函数原型为:

#include<sys/types.h>

#include<unistd.h>

pid_t getpgrp(void)

调用该函数可以返回调用该函数的进程所在的进程组ID。在进程组中有一个特殊的进程,该进程ID与进程组的ID相同。

每一个进程都有其生命期,从创建进程到进程终止,这是一个进程的生命期,而进程组的生命期是从该进程组的创建到最后一个进程终止。在Linux系统中,可以使用setpgid()函数创建一个新的进程组,或者将一个进程加入到一个进程组中,该函数的原型为:

#include<sys/types.h>

#include<unistd.h>

int setgpid(pid_t,pid_t pgid)

当该函数setpgid()调用成功时,返回值为0;当调用失败时,返回值为-1。

7.3.2 时间片分配

在操作系统中,多个进程看似是同时运行的,实质上十多个进程之间不断地切换,每个进程运行一段时间,然后切换到下一个进程执行一段时间,这个所谓的“一段时间”就是一个时间片。多个进程之间进行切换,需要很好地调度策略。

1)时间片轮转调度策略

2)有钱选调度策略(抢占式和非抢占式)

在Linux系统中,提供了几个函数,用于设置和获取进程的调度策略等信息。在设置进程的调度策略时,可以通过参数pid确定需要设置的进程,通过参数policy设置调度策略,然后通过参数param保存进程的调度参数。

#include<sched.h>

int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param)

int sched_getscheduler(pid_t pid)

关于优先级调度策略,在Linux系统中也提供了一些关于优先级的操作函数,如nice()函数用于改变进程的动态优先级;setpriority和getpriority()函数用于设置和获取进程的动态优先级。

7.4 线程

7.4.1 线程概述

线程,又称轻量进程,代表一个进程中某个单一顺序的控制流。线程是进程中的一个实体,是被系统独立调度和分配的基本单位。

一个进程的若干个线程是共享进程中所拥有的全部资源的,每个线程本身不拥有系统资源,只拥有少量的必备资源,如程序计数器、寄存器、栈等。

在Linux多处理器系统中,不同线程可以同时运行在不同的CPU上,一个线程可以创建和终止另外一个线程,一个进程中的多个线程可以并发执行。

7.4.2 线程的属性

在Linux系统中,每一个线程都拥有一个自身的属性,用于代表该线程的特性,而一个进程中的多个线程也有其共同的属性。

1)摧毁与初始化线程属性对象

当使用一个现成的属性对象前,需要首先初始化该对象,然后才可以对现成的属性进行设置和修改。初始化线程属性的函数原型为:

#include<pthread.h>

int pthread_attr_init(pthread_attr_t *attr)

若该函数调用成功,返回值为0;若调用失败,则返回非0值。

pthread_attr_init()函数必须在创建贤臣函数之前调用,可以使用函数中参数attr的属性来初始化线程属性的对象。

pthread_attr_destory()函数的功能是摧毁attr所指向的线程属性对象。销毁的attr属性对象可以使用上述初始化函数重新初始化,该摧毁属性对象的函数原型为:

#include<pthread.h>

int pthread_attr_destory(pthread_attr_t *attr)

参数attr是pthread_attr_t结构体类型。该结构体类型中定义的attr参数的属性如下:

typedef struct

{

  int __detachstate;  /*线程的分离状态*/

  int __schedpolicy;  /*线程调度策略*/

  struct sched_param_schedparam;  /*现成的调度参数*/

  int __inheritsched;  /*线程的继承性*/

  int __scope;  /*现成的作用域*/

  size_t __guardsize;

  int __stackaddr_set;

  void *__stackaddr;  /*线程堆栈位置*/

  unsigned long int __stacksize;  /*线程对线大小*/

}pthread_attr_t;

在pthread_attr_t结构体类型中定义了上述线程属性,这些属性的意义如下:

①detachstate 若表示线程的可连接状态,可以取值为PTHREAD_CRETE_JOINABLE;若表示现成的分离状态,可以取值为PTHREAD_CREATE_DETACHED

②schedpolicy:该变量表示现成的调度策略,当取值为SCHED_OTHER时,属性表示普通,非实时的调度策略;若取值为SCHED_RR,属性表示实时、轮转的调度策略;当取值为SCHED_FIFO时,属性表示实时、先进先出的调度策略。

③schedparam:该比阿娘代表现成的调度参数,改制由线程的调度策略决定。

④inheritsched:表示线程的继承性。当取值为PTHEAD_EXPLICIT_SCHED时,表明从父进程处继承调度属性;当取值为PTHEAD_INHERIT_SCHED时,表明从父进程继承。

⑤scope:该变量表示线程的作用域,当取值为PTHREAD_SCOPE_SYSTEM时,表明每个线程占用一个系统时间片。

2)设置与获取线程的分离状态

#include<pthread.h>

int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate)

int pthread_attr_getdetachstate(pthread_attr_t* attr, int detachstate)

3)设置与获取线程属性对象的调度策略

#include<pthread.h>

int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy)

int pthread_attr_getschedpolicy(pthread_attr_t* attr, int policy)

7.5 进程的特殊操作

7.5.1 获取进程标识

1)获取进程ID和父进程ID

#include<sys/typed.h>

#include<unistd.h>

pid_t getpid(void)

pid_t getppid(void)

2)获得用户ID和有效用户ID

#include<sys/types.h>

#inclued<unistd.h>

uid_t getuid(void)

uid_t geteuid(void)

这两个函数没有调用失败的时候,总是调用成功。

3)获取组ID和有效组ID

#include<sys/typed.h>

#include<unistd.h>

gid_t getgid(void)

gid_t getegid(void)

这两个函数没有调用失败的时候,总是调用成功。

7.5.2 设置进程标识

#include<sys/types.h>

#inclued<unistd.h>

int setuid(void)

int setgid(void)

7.6 小结

本章中详细讲解了进程的概念和属性,并对进程的基本操作进行了举例说明,理论结合实际对进程的各种操作进行了演示,同时,还对多个进程间的相关概念进行了说明。线程作为进程内部的一个小单位,也对其进行了介绍。在前面几节中了解了进程的基本操作,为了拓展对进程的认识,在7.5节中又对进程的接特殊操作进行了理论结合实例的讲解。

通过本章的学习,希望读者对进程控制有一个更加全面深刻的认识。同时,希望读者对于Linux系统的帮助命令“man”的使用能够更加灵活,使得学习Linux C不再困难。

0 0