进程与线程小笔记

[TOC]

资料整理自《嵌入式Linux应用程序开发标准教程》.

进程

什么是进程

进程是一个程序的一次执行过程, 同时也是资源分配的最小单元. 进程和程序是有本质区别的, 程序是静态的, 它是一些保存在磁盘上的指令的有序集合, 没有任何执行的概念; 而进程是一个动态的概念, 它是程序执行的过程, 包括了动态创建, 调度和消亡的整个过程. 它是程序执行和资源管理的最小单元.

进程的标识

Linux下用进程号PID(Process Identity Number)来表示, 还有父进程号PPID. getpid()函数可以返回进程号, getppid()返回父进程号. shell下ps命令可以列出运行的进程.

进程使用资源

每个进程运行在独立的虚拟地址空间,…, Linux进程包含三个段, 分别为”数据段”, “代码段”和”堆栈段”.
“数据段”存放的是全局变量, 常数以及动态数据分配的数据空间, 根据存放的数据, 数据段有可以分成普通数据段(可读可写/只读数据段, 静态初始化的全局变量或常量), BSS数据段(未初始化的全局变量)以及堆(存放动态分配的数据).
“代码段”存放的是程序代码的数据.
“堆栈段”存放的是子程序的返回地址, 子程序的参数以及程序的局部变量等.

后面的数据空间描述其实对C语言都是成立的, 这里要注意的是“堆”是”数据段”的一部分. 函数中的static类型变量是算”普通数据段”的吗?

进程编程

创建新进程的唯一方法是使用fork()函数….fork()函数用于从已存在的进程中创建一个新进程. 新进程称为子进程, 而原进程称为父进程. 使用fork()函数得到的子进程是父进程的一个复制品, 它从父进程处继承了整个进程的地址空间, 包括进程上下文, 代码段, 进程堆栈, 内存信息, 打开的文件描述符, 信号控制设定, 进程优先级, 进程组号, 当前工作目录, 根目录, 资源限制和控制终端等, 而子进程所独有的只有它的进程号, 资源使用和计时器等….fork()函数看起来执行一次却有两个返回值.

我看了这段的话主要是有两个疑问, 第一个就是最后一句的”执行一次有两个返回值”, 然后就是”上下文件代码段”这个是全部复制的, 这些是什么个情况呢?还是先直接看一个例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
result = fork();
if (result == -1)
{
printf("Fork error\n");
}
else if(result == 0)
{
printf("The return value is %d\n
In child process!!\nMy PID is %d\n", result, getpid());
}
else
{
printf("The returned value is %d\n
In father process!!\nMy PID is %d\n", result, getpid());
}
return result;
}

然后这个会输出的是:

1
2
3
4
5
6
7
8
$ gcc fork.c -o fork
$ ./fork
The return value is 76
In father process!!
My PID is 75
The return value is 0
In child process!!
My PID is 76

然后这个就是解释了两次返回值这个事情了. 剩下的就是全部复制这个问题, 按这个说法, fork()函数是不是也是要复制的, 复制之后执行的话不就是一个无穷迭代的过程了么…

实际上是在父进程中执行fork()函数时, 父进程会复制出一个子进程, 而且父子进程的代码从fork()函数的返回处开始分别在两个地址空间中同时运行. 从而两个进程分别获得其所属fork()的返回值, 其中在父进程中返回值是子进程的进程号, 而在子进程中返回0.

答案是这个.

新建一个进程的目的自然是要在新进程里面做不同的事情, 这个要怎么做呢, 思路就是要利用进程的返回值来做分支:

1
2
3
4
if (fork() == 0)
{
/*arguments*/
}

这个/*arguments*/里面放的就是子进程跟父进程里面处理的不同的东西.

进程通信

Linux的进程通信有这么几种:

  1. 管道(Pipe)及有名管道(Named Pipe): 管道可用于具有亲缘关系的进程间通信, 有名管道, 除具有管道的功能外, 还允许无亲缘关系的进程间通信.
  2. 信号(Signal): 信号是在软件层次上对中断机制的一种模拟, 它是比较复杂的通信方式, 用于通知进程有某事件发生, 一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的.
  3. 消息队列(Message Queue): 消息队列是消息的链接表, 包括POSIX的消息队列, SystemV消息队列. 它克服了前两种通信方式中信息量有限的缺点, 具有写权限的进程可以按照一定的规则向消息队列中添加新消息; 对消息队列有读权限的进程则可以从消息队列中读取信息.
  4. 共享内存(Shared Memory): 可以说这是最有用的进程间通信方式. 它使得多个进程可以访问同一块内存空间, 不同进程可以及时看到对方进程中对共享内存中的数据更新. 这种通信方式需要依靠某种同步机制, 如互斥锁和信号量等.
  5. 信号量(Semaphore): 主要作为进程之间以及同一进程的不同线程之前的同步互斥手段.
  6. 套接字(Socket): 这是一种更为一般的进程间通信机制, 它可用于网络中不同机器之前的进程间通信, 应用非常广泛.

同步, 互斥, 临界资源

在多任务操作系统环境下, 多个进程会同时运行, 并且一些进程之间可能存在一定的关联. 多个进程可能为了完成同一个任务会相互协作, 这样形成进程之间的同步关系. 而且在不同的进程之间, 为了争夺有限的系统资源会进入竞争状态, 这就是进程间的互斥关系.
进程之间的互斥与同步关系存在的根源在于临界资源. 临界资源是在同一时刻只允许有限个(通常只有一个)进程可以访问(读)或(修改的资源), 通常包括硬件资源和软件资源. 访问临界资源的代码叫做临界区, 临界区本身也会成为临界资源.

这个应该是操作系统的内容.

线程

…线程是进程内独立的一条运行路线, 处理器调度的最小单元, 也可以称为轻量级进程. 线程可以对进程的内存空间和资源进行访问, 并与同一进程中的其他线程共享….由于线程共享了进程的资源和空间, 因此, 任何线程对系统资源的操作都会给其他的线程带来影响. 由此可知, 多线程中的同步是非常重要的问题.

处理器调度, 这个”调度要怎么理解”?

线程机制分类

有这么三类: 用户级, 轻量级, 内核级.

使用线程机制大大加快上下文切换速度而且节省很多资源. 但是因为在用户态和内核态均要实现调度管理, 所以会增加实现的复杂度和引起优先级翻转的可能性. 一个多线程程序的同步设计与调试也会增加程序实现的难度.

线程编程

在Linux中, 一般pthread线程库是一套通用的线程库, 是由POSIX提出的, 因此具有很好的可移植性.

就是用的pthread函数族: pthread_create(), pthread_exit(), pthread_join(), pthread_cancel()等.

线程同步机制

由于线程共享进程的资源与地址空间, 因此在对这些资源进行操作时, 必须考虑到线程间资源访问的同步与互斥问题….POSIX的两线程同步机制, 分别是互斥锁与信号量. 这两个同步机制可以互相通过调用对方来实现, 但互斥锁更适合用于同时可用的资源是唯一的情况; 信号量更适合用于同时可用的资源为多个的情况.

互斥锁是用几种简单的加锁方法来控制对共享资源的原子操作. 这个互斥锁只有上锁和解锁两种状态…在同一时刻只能有一个线程掌握某个互斥锁…若其他线程希望上锁一个已经被上锁的互斥锁, 则该线程就会挂起, 直到上锁的线程释掉互斥锁为止.

这么一讲, 互斥锁其实有点像数字电路的总线管理机制, 同一时刻总线上只能有一条输入输出路径.

信号量在进程分类里面是已经讲到过的了, 这里的信号量其实是一个思想的, 也是PV原子操作.

PV原子操作是对整数计数器信号量sem的操作. 一次P操作使sem减一, 而一次V操作使sem加一. …当信号量sem的值大于等于0时, 该进程(或线程)具有公共资源的访问权限; 相反, 则该进程(或线程)就将阻塞直到信号量sem的值大于等于0. …若用于互斥, 几个进程(或线程)往往只设置一个信号量sem, …当信号量用于同步操作时, 往往会设置多个信号量, 并安排不同的初始值来实现它们之间的顺序执行.

小结

进程与线程是程序设计的大概念, 在学校学这些语言设计的时候都没有接触到… 这些是在另外一个层级上面的东西, 以我这个门外汉看来, 掌握了这些才能实现系统级的设计.

这次在网上又学到了新东西, 总结起来就是:

对于 Windows 系统来说, 新建进程的开销很大, 因此Windows鼓励使用多线程编程. Windows多线程学习重点是要大量面对资源争抢与同步方面的问题.
对于 Linux 系统来说, 新建进程的开销很小, 因此Linux鼓励使用多进程编程. 因此, Linux下的学习重点是进程间通讯的方法.
大量创建进程的典型例子有两个, 一个是gnu autotools工具链, 用于编译很多开源代码的, 他们在Windows下编译速度会很慢, 因此软件开发人员最好是避免使用Windows. 另一个是服务器, 某些服务器框架依靠大量创建进程来干活, 甚至是对每个用户请求就创建一个进程, 这些服务器在Windows下运行的效率就会很差. 这”可能”也是放眼全世界范围, Linux服务器远远多于Windows服务器的原因.

这个已经是另外一个层面的知识, 也反映了进程/线程编程是很有难度的设计层面的东西.