进程控制与进程间通信

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yanhe156/article/details/84348249

在学习这部分之前,我对Linux系统基本不了解,只是做一些简单的工作,使用一些常见命令,使用Makefile编译工程,做arm交叉编译等。

所以这部分内容也会对用到的相关内容做一些总结。

1 进程

1.1 进程(Process)

程序本身是指令的有序集合,进程是程序在处理器上的一次执行过程。

程序中包含了创建进程需要的信息。

A program is a file containing a range of information that describes how to construct
a process at run time.

从内核角度来看,process包含user-space中为程序和变量分配的存储,和内核中的一些数据结构。

和进程相关的信息保存在 task_struct 结构体中,这个结构体定义在linux/sched.h中。

但《The Linux Programming Interface》中没有task的概念,有process,thread的概念,PID指的就是Process ID。除了一些system process,PID和程序没有固定的关系,也就是运行一个程序时创建的进程是不固定的。PID数值是有上限的,可以调整。

Process & Thread:

A process is an instance of an executing program.

A single process can contain multiple threads.

All of these threads are independently executing the same program, and they all share the same global memory, including the initialized data, uninitialized data, and heap segments. (A traditional UNIX process is simply a special case of a multithreaded processes; it is a process that contains just one thread.)

这里还没有具体看进程的属性和生命周期等概念。

1.2 创建进程

1.2.1 理解进程的创建

创建进程对使用者来说有两种方式:

  • 在shell中执行命令或可执行文件,其实是由shell进程调用了fork函数创建子进程
  • 在自己写的代码中调用fork函数来创建子进程

pstree 命令可以通过一个树的方式,列出系统中的所有进程。可以看到下图中所有的进程都是由init进程创建的(systemd是init进程的一种实现)。init进程即进程1,,其PID固定为1,它是由进程0(PID为0的进程)创建的,进程0由内核创建。进程0在创建进程1后,转换为交换进程或空闲进程。

Linux系统中除了进程0,所有的进程都是由父进程调用fork函数创建的。
在这里插入图片描述

1.2.2 fork函数

函数原型为pid_t fork(void), 头文件为unistd.h

pid_t 就是 unsigned int

在程序中,fork(); 这条语句之后的程序(包括fork()这个语句)会以两个进程来运行。之前的不受影响。同时fork 函数在被调用后,在子进程和父进程中的分别返回,返回值不同。 fork函数有三种返回值:

  1. 子进程中返回值为0 ,提提示当前运行在子进程中
  2. 父进程中返回值为子进程PID,让父进程掌握所创建子进程的PID
  3. 出错返回-1

因此可以判断fork函数的返回值,从而在父进程和子进程中执行不同的程序。使用fork函数时,返回值非常重要。

父子进程的异同:

  • 相同:

    1. 环境变量
    2. 打开的文件
    3. 相同的用户ID
  • 不同

    1. fork返回值

    2. 进程ID

    3. 子进程的tms_utime,tms_stime,tms_cutime,tms_ustime都被清零

1.2.3 fork的应用场景

  1. 希望复制父进程(共享代码,复制数据空间),但父子进程通过条件语句执行相同代码中的不同分支。

    比如:网络服务程序,父进程等待客户端的服务请求,当请求到达后,就调用fork创建一个子进程来处理该请求。而父进程继续等待下一个服务请求。

  2. 父子进程执行不同的可执行文件。即在子进程中,调用exec类函数执行另一个可执行文件。

    vfork函数用于创建新进程,目的是执行另一个可执行文件。vfork函数保证子进程先运行。

1.3 进程的终止

正常终止:

  1. main函数返回
  2. 调用函数exit(), #include<stdlib.h>
  3. 调用函数_exit(),#include<unistd.h>

exit()和 _exit()的区别在于_exit()会直接中止程序,而exit()会先“刷新I/O缓冲”。

关于I/O缓冲:比如printf()执行后,此时会立刻执行下一条语句,而不是等待I/O完成后再执行下一条语句,以此来节省系统调用的次数,从而节省程序运行时间。而具体的write()系统调用是等到I/O缓冲满足某些条件后才会实际执行。

I/O缓冲区属于内存,所以速度很快。即使我们不是对硬盘进行读写,是输出到控制台,肯定也同样需要时间,显示器也是属于外设,不可能比访问内存更快。

img

I/O缓冲区在特定的条件下才会执行系统调用write()来向外设进行写程序。I/O缓冲区也有不同的类型,不同类型的I/O缓冲执行系统调用的条件不同,键盘,显示器这种字符设备是行缓冲,因此会在缓冲区中出现\n时执行系统调用。

可使用int fflush (FILE *__stream); 来手动刷新缓冲区。

异常终止:

  1. abort()函数
  2. 进程接收到某信号

1.4 子进程的结束

1.4.1 获知子进程运行状态

子进程运行结束后,不管正常或异常,都需要等待父进程来回收它的状态信息,之后才会从进程分配表中消失,也就是子进程结束,同时子进程的PCB等内核空间资源才被释放。对于已经终止,但父进程尚未对其调用wait函数或waitpid函数的进程,其状态为TASK_ZOMBIE,称为僵尸进程。

回收子进程状态信息的函数有:

// sys/wait.h
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);

pid_t wait(int *stat_loc) 其返回值是子进程的id,*stat_loc其实也算是一个返回值,wait()函数内会对其进行赋值,其意义是子进程的状态。 当传入空指针时,只是为了等待子进程终止,传入非空指针时,才会将子进程状态改变信息存放在它指向的存储空间中。

系统提供一些带参宏来解析 stat_loc。

父进程是有可能在子进程终止之前终止的,因为父子进程是独立的,没有关联。此时子进程的父进程将变为init进程,由init进程进行回收。

1.4.2 wait函数和waitpid函数的区别

如果一个进程有多个子进程,只要有一个子进程状态改变,那么wait函数就会返回。 此时可以通过返回的PID来判断是不是我们想要的子进程,不是的话,则循环调用wait函数。

waitpid函数可以等待某个特定子进程的状态改变,第一个参数是PID,第三个参数是等待的选项,可以为0,也可以设为三种常量,实现特定的功能,比如可将waitpid设置为非阻塞。waitpid显然也可以传入适当的参数实现和wait函数相同的功能。

wait函数功能:等待一个子进程状态改变,会阻塞父进程(实现同步)。

waitpid函数功能

  1. 等待一个特定进程的状态改变

  2. 实现非阻塞的等待操作,只是取得子进程状态改变信息,不等待其改变

  3. 支持进程组的控制(通过第一个参数)

1.5 执行一个新程序

  • execute 系列函数。有6个函数,函数名以exec开头,之后为4个字母的组合。四个字母是:
    1. l 表示list,每个命令行参数作为一个单独的参数
    2. v 表示vector,命令行参数放在数组中
    3. e 表示Environment,表示由函数调用者提供环境变量
    4. p 表示PATH,表示通过环境变量来指定路径,查找可执行文件

1.5.1 execve函数

// unistd.h
int execve(const char *path, const char *argv[], const char *envp[]);

execve函数是内核级系统调用,其他函数都是依赖execve函数。

execve()启动的新程序会覆盖当前程序的内存空间,execve()如果返回负值,表示调用失败。如果调用成功,就直接执行新程序,不会返回。

The most frequent use of execve() is in the child produced by a fork(), although
it is also occasionally used in applications without a preceding fork().

注意 char *argv[]char *envp[]是字符指针的数组。我还没理解第三个参数具体做什么,似乎是用于参数传递。

TLPI是这么写的:

The envp argument corresponds to the environ array of the new program;

unistd.h的源码,其中写了一句extern char **environ; 。没有看到execve函数的具体实现,但是应该是在这个函数中,将全局变量environ赋值为envp。 environ是个全局变量。在被启动的程序中,可以声明extern **environ,然后就可以取得启动它的程序传过来的参数。

关于char **achar *b[] 的区别:

char **a 是多级指针,只能用指针为其赋值,比如char **a = b; 这样是没有问题的。但是不能直接用字符串为其赋值。

char *b[] 是一个字符串常量的数组,因此可以这样赋值:char *b[] = {"12","34",NULL};char *定义的是字符串常量。)

例子:

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

int main(int argc, char *argv[])
{
        char *envVec[] = {NULL};

        char *argVec[] = {"echo", "Hello World",NULL};

        char **tt = argVec;
		//execve("/bin/echo", argVec, envVec); 
        //Have same result as the next line.
        execve("/bin/echo", tt, envVec);
        return 0;
}

1.5.2 其他函数

比如:

int execl(const char *pathname,const char *arg0, ...,NULL);

可以看到这个函数exec后是字母l,以可变参数的形式传入多个命令行参数,以空指针NULL结束。同时这个函数没有传入新的环境变量。

2 线程(Thread)

  • 本来主要是为了做作业,看进程的相关概念。但为了避免线程和进程概念混淆,以及为了更好地理解进程,所以把线程相关概念也看了。

进程是资源分配的单位。同一个进程中的不同线程可以共享进程的全局变量,打开的文件等等。

对没有通过程序显式创建线程的程序,运行时可以看成只有一个线程的进程。

进程操作和线程操作:

在这里插入图片描述

线程操作的头文件是 pthread.h。

线程ID的类型是pthread_t ,实际上是unsigned long int , 类型定义在 /usr/include/bits/pthreadtypes.h中。

3 进程间通信

3.1 Interprocess Communication (IPC)

The Linux Programming Interface 的Chapter 43开始对IPC的介绍。

This chapter presents a brief overview of the facilities that processes and threads
can use to communicate with one another and to synchronize their actions.

也包括了threads的通信.

按照功能分三个大类:

  1. communication
  2. signal
  3. synchronization

IPC现在仍有两种标准: System V和POSIX。
两种标准IPC实现方式有所不同,但是都有消息队列,信号量和共享内存。
对用户(我)来说,现在比较关心的是如何使用。
见我的另一篇总结 对进程的理解和POSIX 信号量的使用

参考:

[1] 《The Linux Programming Interface》
[2] 操作系统
[3] linux下多种锁的比较
[4] Linux操作系统编程

猜你喜欢

转载自blog.csdn.net/yanhe156/article/details/84348249