Linux 下进程与线程的基本概念

2019-10-01

关键字:进程、线程、信号量、互斥锁


什么是程序?

程序就是存放在磁盘上的指令和数据的有序集合,就是源代码编译产物。 它是静态的。

什么是进程?

进程就是操作系统为执行某个程序所分配的资源的总称。进程是程序的一次执行过程,因此它与程序不同,它是动态的。它的生命周期包括创建、调度、执行和消亡。

进程的内容主要包括以下三个部分:

1、正文段;

2、用户数据段;

3、系统数据段。 

其中正文段与用户数据段两部分是从程序当中来的。而系统数据段则是操作系统分配的用来管理这个进程用的。

系统数据段中主要包含三个部分:

1、进程控制块;

process control block。用于存放和进程相关的属性,主要包括以下四个部分:

1、进程标识ID,简称 PID;

2、进程用户;

3、进程状态、优先级;

4、文件描述符集;

2、CPU寄存器值;

3、堆栈。

进程的类型有以下三种:

1、交互进程;

在 shell 下启动,运行在前台或后台。

2、批处理进程;

和终端无关,被提交到一个作业队列中以便顺序执行。

3、守护进程;

和终端无关,一直在后台运行,因而生命周期很长。直到操作系统关闭才会结束。

进程的状态有以下四种:

1、运行态;

进程正在运行或准备运行;

2、等待态;

进程正在等待某一个事件或某种系统资源,当条件满足时才被唤醒执行;等待态又可分为可中断型不可中断型两种。

3、停止态;

进程被中止,收到信号后可继续运行。

4、死亡态;

进程已经完全结束,但pcb没有被释放。又称为僵尸态。

查看进程信息可以通过以下几种方式:

1、ps;

用于查看系统进程当前的快照。

ps -ef   -->  查看当前所有进程的信息。

ps aux  -->  可以看到进程的当前状态。

ps aux -L  -->  可以看到进程的线程信息。

更多关于 ps 的参数及数值的含义可以通过 man ps 来寻求帮助。

2、top;

查看进程当前的动态信息。

一般用于查看进程占用资源的情况。

3、/proc;

这是一个目录,这个目录下存放着系统当前进程的所有信息。

通过 ps 命令查到每个进程的进程号,然后 /proc 目录下就会有和进程号相同的目录,这个目录下存放着的就是和这个进程相关的所有信息。

其它与进程相关的命令:

1、nice;

按指定的优先级运行进程。

进程的优先级范围为 -20 ~ 19,数字越小优先级越高,默认优先级为0。

例如:以既定的优先级启动程序:nice -n 9 ./test   表示 test 进程将以优先级9来启动。

普通用户只能设定 0~19 的优先级,只有管理员用户才能设置负数优先级。

2、renice;

改变正在运行的进程的优先级。

例如:renice -n 7 29070   后面跟的是要修改的 PID。

3、jobs;

查看当前的后台进程。

4、fg;

将后台作业变为前台运行。

例如:fg 2   将作业号为2的后台进程变成前台进程。

5、bg;

将处于挂起状态的后台进程运行起来。

例如 bg 2,将作业号为2的后台挂起进程在后台运行起来。

6、如何将一个进程在后台运行?

主要有两种方式。

一种是在运行程序之前以 '&' 符号结尾。以该符号结尾的进程在执行以后将直接在后台运行而不会阻塞命令行。

另一种方式则是进程在前台运行时按下组合键 'ctrl + z' 。此时该前台进程将转为后台进程,但当前是处于停止态的,若想恢复进程的运行,则需要执行上述第 5 条的 bg 命令来唤醒进程。

何如创建进程?

#include <unistd.h>

pid_t fork(void);

创建失败时返回值 -1,创建成功时父进程返回子进程的进程号,子进程返回值 0。

子进程继承了父进程的几乎所有内容,但二者拥有独立的空间,运行时互不干扰。

当父进程结束后,子进程即成为孤儿进程,会由 init 进程收养。同时子进程会变为后台进程。

当子进程先于父进程结束时,父进程若没有及时回收,则子进程会变成僵尸进程。

子进程被创建以后会从 fork 函数的下一条语句开始执行。

如何结束一个进程?

#include <stdlib.h>

#include <unistd.h>

void exit(int status);

void _exit(int status);

exit() 函数结束进程前会刷新流缓冲区,而后面 _exit() 函数则不会。

关于 exec 函数族

进程在正常运行过程中可以通过调用 exec 函数族来执行另一个程序。

通过 exec 函数族执行了另一个程序后,进程的当前内容就会被替换,简单理解成从此该进程就属于另外一个被启动的程序了。因此,我们可以通过 exec 函数族来让父子进程执行不同的程序。

#include <unistd.h>

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

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

下面是一个例子:

if(execl("/bin/ls", "ls", "-a", "-l", "/etc", NULL) < 0){
    perror("execl");
}

if(execlp("ls", "ls", "-a", "-l", "/etc", NULL) < 0){
    perror("execl");
}

这两个例子都表示要执行一条 shell 命令: ls -a -l /etc。 两个函数的最后一个参数都必须为 NULL。

两个函数的区别就是 execlp 函数会去 $PATH 中查找程序运行,而 execl 不会。因此 execlp 在执行 ls 命令时可以省略完整路径名称。

#include <unistd.h>

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

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

与上面 execl 就最后一个字母不同。 l 其实代表 list,表示传的参数通过一个可变参数形式传入。这里的 v 则表示 vector,是数组之意。

不过在使用方式上就大同小异了。

char *arg[] = {"ls", "-a", "-l", "/etc", NULL};

if(execv("/bin/ls", arg) < 0){
    perror("/bin/ls");
}

if(execvp("ls", arg) < 0){
    perror("/bin/ls");
}

在程序中执行 shell 命令的函数:

#include <stdlib.h>

int system(const char *command);

执行成功后会返回 command 命令的返回值,失败会返回 EOF。

执行该命令后当前进程会阻塞直到 command 命令执行完毕为止。

进程回收:

#include <unistd.h>

pid_t wait(int *status);

执行成功时返回被回收的子进程PID,失败时返回 EOF,并设置 errno。

若子进程没有结束父进程就去回收它,父进程就会阻塞直至回收成功。

若父进程创建了多个子进程,则哪个子进程先结束就先回收哪个。理论上来讲,要回收多少个子线程就得调用多少次 wait() 函数。

参数 status 用于保存子进程的返回值和结束方式的地址,若传入 NULL,表示直接释放 PCB,不接收返回值。

进程的返回值和结束方式:

一个进程可以通过三种方式来正常结束:

1、exit

2、_exit

3、return ( 0 ~ 255 )

父进程调用 wait(&status) 来接收子进程的结束结果。当 wait() 函数执行过去以后,可以通过几个系统宏来提取 status 中的信息。

1、WIFEXITED(status)

判断子进程是否正常结束。

2、WEXITSTATUS(status) 

获取子进程返回值。

3、WIFSIGNALED(status) 

判断子进程是否是被信号非正常结束的。

4、WTERMSIG(status)

提取结束子进程的信号类型。

另一种进程回收的方式:

#include <unistd.h>

pid_t waitpid(pid_t pid, int *status, int option);

对指定进程以指定的方式进行回收。这种回收子进程的方式要比 wait() 灵活一点。

执行成功时返回被回收的子进程PID或0,失败时返回 EOF。

pid 值是具体的子进程值,也可以是 -1,-1表示任意一个子进程。

option有两种值,0 或宏 WNOHANG。 0表示阻塞,另一是不阻塞。

守护进程

通常在操作系统启动时运行,要待操作系统关闭时才会退出。

在linux系统中有大量守护进程,很多服务程序都以守护进程的形式在运行。

守护进程的三大特点:

1、后台运行;

2、与终端无关; 无法通过终端输入输出。

3、周期性地执行某种任务或等待处理特定的事件。

关于会话与控制终端:

Linux 以会话、进程组的方式管理进程。

每个进程都属于一个进程组。

会话是一个或多个进程组的集合。通常用户打开一个终端时就会创建一个会话,所有通过该终端运行的进程都属于该会话。 

终端关闭时,所有相关进程都会结束。守护进程与终端无关,才能在终端关闭时继续运行。

守护进程的创建:

守护进程的创建是需要依赖于交互进程的。

通常的做法是在交互进程中创建一个子进程,然后将父进程退出,此时子进程就变成孤儿进程,但仍继续运行在后台。第二步是在子进程中创建新的会话:setsid() 函数可以实现。此时这个子进程将会成为新会话的组长,并且脱离原先会话的管控,即此时已经与旧终端无关了,它同时也不和任何一个终端相关了。第三步是修改当前工作目录: chdir("/") 函数可以实现,参数是要切换的工作路径。 第四步是修改文件权限掩码:umask(0) 函数可以实现 即将掩码值设为0,不限制任何权限位,这个掩码的设置只对当前进程有效,只是局部的。第五步是要将父进程打开的文件描述符都关闭,通常使用如下的关闭方式:

for(i = 0; i < getdtablesize(); i++){
    close(i);
}

守护进程示例:

创建一个守护进程,每隔1秒将当前时间值写入 time.log 文件中。

int main(){
  pid_t pid;
  FILE *fp;
  time_t t;
  int i;

  if((pid = fork()) < 0){
    perror("")
  }  else if(pid > 0){
    exit(0);
  }


  setsid();
  umask(0);
  chdir("/tmp");
  for(int i = 0; i < getdtablesize(); i++){
    close(i);
  }
  fp = fopen("time.log", "a")
  while(1){
    time(&t);
    fprint(fp, "%s", ctime(&t));
    fflush(fp);
    sleep(1);
  }


}

线程

进程间的切换资源开销很大,因此就出现了线程。 线程还有一个别称:轻量级进程,英文缩写LWP。

同一个进程中的线程是共享相同的地址空间的。使用多线程开发能够大大提升任务切换的效率,说白了就是能让程序运行的更快。

linux 不区分进程、线程,这些东西在 linux 里都被视为任务。

线程是由进程创建并管理的,所以当进程结束时,其下的所有线程都会结束。

线程的创建:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*routine)(void *), void *arg);

函数执行成功时返回值 0,失败返回相应错误码。

thread 代表一个线程对象。

attr 表示一个结构体指针,它表示的是线程属性,NULL 代表默认属性。

第三个参数是一个函数指针。它有一个 void * 的参数,同时返回值也是 void *。这个函数指针就是你想让哪个函数作为子线程来执行。

第四个参数 arg 就是要传递给 routine 函数的参数。

结束线程:

#include <pthread.h>

void pthread_exit(void *retval);

结束当前线程。线程结束的时候该线程所创建的所有资源都会被释放。

线程的回收:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

执行成功返回值 0,失败返回相应错误码。

thread 就是要回收的线程的对象。

retval 就是用来接收线程返回值的指针的地址。

该函数是一个阻塞函数,调用后会一直阻塞直到线程结束返回。

线程间通信:

线程是共享同一进程的地址空间的,拟线程间的通信将会很容易,直接就可以通过全局变量来交换数据。但这种访问的便利性也带来了一些风险,通常当有多个线程访问相同的共享数据时需要同步互斥锁

同步机制:

同步指的是多个任务按照事先约定的顺序先后地完成一件事情。

同步机制里最常用的就是 信号量。信号量可以决定线程当前是继续运行还是等待。

信号量代表某一类资源,其值表示系统中该资源的数量。因此它是一个非负数的值。

信号量是一个受保护的变量,只能通过三种操作来访问:

1、初始化;

2、P操作(申请资源);

3、V操作(释放资源)。

信号量的PV操作:

当进行P操作时,它会去判断当前信号量的值是否大于0,若是,则申请P操作的任务继续运行,同时信号量的值减一。若否,则阻塞。V操作则是先让信号量的值加一,再判断当前是否有正在等待资源的任务以让它继续运行。

信号量的类别:

POSIX定义了两种信号量:

1、无名信号量

它是一种基于内存的信号量。通常都用于进程内部线程之间的通信。

2、有名信号量

既可以用于进程之间也可以用于线程之间。

pthread 库中对信号量常用的操作:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);  信号量的初始化。

int sem_wait(sem_t *sem); P操作

int sem_post(sem_t *sem); V操作

sem_init 函数:

执行成功返回0,失败返回EOF,并设置 errno。第一个参数指向要初始化的信号量对象第二个参数表示是进程间还是线程间,0表示线程间,1表示进程间。第三个参数则是信号量的初值了。

线程的互斥:

临界资源:它是同一时刻只允许一个任务访问的共享资源。

临界区:访问临界资源的代码。

互斥机制:mutex 互斥锁。互斥锁的特性就是同一时刻最多只能被一个任务所持有。它的状态要么是空闲状态,要么是被某个任务所持有。任务访问临界资源前先申请锁,访问完了以后释放锁。

互斥锁的初始化

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

执行成功时返回值0,失败时返回对应错误码。

mutex 即指向要初始化的互斥锁的指针。

attr 用以设置锁的属性,传 NULL 表示使用默认属性。

申请互斥锁

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

成功返回0,失败返回对应错误码。

mutex 就是你想去申请的锁。当这个锁处于空闲状态时,申请锁的任务即拿到了这个锁,任务可以继续执行,否则这个任务就只能阻塞起来了。

释放互斥锁

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);

与上面的申请锁相对应。


猜你喜欢

转载自www.cnblogs.com/chorm590/p/20191001_.html