微信搜索“编程笔记本”,获取更多信息
------------- codingbook2020 -------------
今天我们来学习一下操作系统方面的知识:程序、进程与线程。
在计算机上运行的程序是一组指令及指令参数的集合,指令按照既定的逻辑控制计算机运行。进程则是运行着的程序,是操作系统执行的基本单位。线程是为了节省资源而可以在同一个进程中共享资源的一个执行单位。
进程的出现最早是在 UNIX 下,用以表示多用户、多任务的操作系统环境下,应用程序在内存环境中的基本执行单元的概念。进程是 UNIX 操作系统环境中的基本概念,是系统进行资源分配的最小单位。 UNIX 操作系统下的用户管理和资源分配等工作几乎都是操作系统通过对应用程序进程的控制实现的。
C、C++、Java 等语言编写的源程序经相应的编译器编译成可执行文件后,提交给计算机处理器运行。应用程序的运行状态称为进程
进程从用户角度来看是应用程序的一个执行过程。从操作系统核心的角度来看,进程代表的是操作系统分配的内存、CPU 时间片等资源的基本单位,是为正在运行的程序提供的运行环境。进程与应用程序的区别在于,应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中,而进程则是处于动态条件下由操作系统维护的系统资源管理实体。
程序与进程最大的不同之处在于:
Linux 的进程操作方式主要有产生进程、终止进程,并且进程之间存在数据和控制的交互,即进程间通信与同步。
进程的产生过程
进程的产生有多种方式,其基本过程都是一致的。
(1) 首先赋值其父进程的环境配置;
(2) 在内核中建立进程结构;
(3) 将结构插入到进程列表,便于维护;
(4) 分配资源给此进程;
(5) 赋值父进程的内存映射信息;
(6) 管理文件描述符和链接点;
(7) 通知父进程。
进程的终止方式
有 5 中方式使进程终止。
(1) 从 main 返回
(2) 调用 exit
(3) 调用 _exit
(4) 调用 abort
(5) 由一个信号终止
进程在终止的时候,系统会释放进程所拥有的资源(内存、文件符、内核结构等)。
进程之间的通信
进程之间的通信有多种方式,其中管道、共享内存和消息队列是最常用的方式。
(1) 管道是 UNIX 族中进程通信的最古老的方式,它利用内核在两个进程之间建立通道,它的特点是与文件的操作类似,仅仅在管道的一端只读,另一端只写。利用读写的方式在进程之间传递数据。
(2) 共享内存是将内存中的一段地址在多个进程之间共享。多个进程利用获得的共享内存的地址来直接对内存进行操作。
(3) 消息队列是在内核中建立一个链表,发送方按照一定的标识将数据发送到内核中,内核将其放入链表中,等待接收方的请求。接收方发送请求后,内核按照消息的标识,从内核中将消息从链表中摘下,传递给接收方。消息队列是一种完全的异步操作方式。
进程之间的同步
多个进程之间需要协作完成任务时,进场发生人物之间的依赖现象,从而引出了进程的同步问题。Linux 下进程的同步方式主要有消息队列、信号量等。
信号量是一个共享的表示数量的值,用于多个进程之间操作或者共享资源的保护,它是进程之间同步的最主要方式。
进程和线程的主要区别与联系如下:
进程是计算机中运行的基本单位。要产生一个进程,有多种方式,例如使用 fork()、system()、exec() 等函数,这些函数的不同之处在于其运行环境的构造,本质都是对程序运行的各种条件进行设置,在系统之间建立一个可以运行的程序。
每个进程在初始化的时候,系统都分配了一个 ID 号,用于标识此进程。在 Linux 中进程号是唯一的,系统可以用这个值来表示一个进程,描述进程的 ID 号通常叫做 PID ,即进程 ID (process id) 。PID 的变量类型为 pid_t
。
getpid()
函数用于获取当前进程的 PID ,getppid()
用于获取当前进程的父进程的 PID 。返回值类型都是 pid_t
,其底层是类型是 unsigned int
。
示例如下:
#include
#include
#include
int main() {
pid_t pid, ppid;
pid = getpid();
ppid = getppid();
printf("当前进程的ID号为:%d\n", pid);
printf("当前进程的父进程ID号为:%d\n", ppid);
return 0;
}
/*
运行结果:
当前进程的ID号为:6295
当前进程的父进程ID号为:5958
*/
产生进程的方式很多,fork() 是其中的一种。fork() 函数以父进程为蓝本复制一个进程,其 PID 与父进程 PID 不同。在 Linux 环境下,fork() 是以写复制实现的,只有内存与父进程不同,其他与父进程共享,只有在组进程或子进程进行了修改后,才重新生成一份。
fork() 的特点是执行一次,返回两次。在父进程中返回的是子进程的 PID ,在子进程中返回的是 0 。若失败则返回 -1 。
示例如下:
#include
#include
#include
int main() {
pid_t pid;
pid = fork();
if (pid == -1) {
printf("进程创建失败");
return -1;
} else if (pid == 0) {
printf("这是子进程。fork返回值:%d\t当前进程ID:%d\t父进程ID:%d\n", pid, getpid(), getppid());
} else {
printf("这是父进程。fork返回值:%d\t当前进程ID:%d\t父进程ID:%d\n", pid, getpid(), getppid());
}
return 0;
}
/*
运行结果:
这是父进程。fork返回值:6604 当前进程ID:6603 父进程ID:5958
这是子进程。fork返回值:0 当前进程ID:6604 父进程ID:6603
*/
system() 调用 shell 的外部命令在当前进程中开始另外一个进程。
system() 函数调用 /bin/sh-c command
执行特定的命令,阻塞当前进程直至 command 命令执行完成。执行 system() 函数时,会调用 fork()、execve()、waitpid() 等函数,其中任意一个调用失败,都会导致 system() 调用失败。当 system() 调用失败时返回 -1 ;当 sh 不能执行时返回 127 ;当调用成功时返回进程状态值。
示例如下:
当前进程的ID号:6869
PING www.a.shifen.com (14.215.177.39) 56(84) bytes of data.
64 bytes from 14.215.177.39 (14.215.177.39): icmp_seq=1 ttl=128 time=112 ms
64 bytes from 14.215.177.39 (14.215.177.39): icmp_seq=2 ttl=128 time=115 ms
--- www.a.shifen.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 112.959/114.045/115.131/1.086 ms
返回值为:0
*/
在上面的示例中,先打印当前进程的 ID 号,随后使用 system() 函数 ping 百度的主机,发送和接收两个 ping 的请求包,再退出 ping 程序。此时 ret 是新进程的返回值,由于 ping 正常退出,新进程返回 0 ,所以打印出来的 ret 也是 0 。
使用 fork() 函数和 system() 函数时,系统会新教一个进程,指向调用者的操作,而原来的进程仍然存在,直到用户显式退出。exec() 族的函数与其不同,它会用新进程替换原有的进程,系统会从新的进程运行,新进程的 PID 与 原进程的 PID 相同。
exec 族函数有 6 个,但只有 execve() 是系统调用,其余 5 个都是在此基础上包装的库函数。exec() 函数族的函数执行成功后不会返回,失败时返回 -1 。
exec() 的一个比较普遍的用法是,先使用 fork() 函数分叉进程,然后在新的进程中建立子进程。
在 Linux 系统中,除了初始进程 init ,所有的进程都有父子或者兄弟关系,没有哪个进程与其他进程完全独立。系统中每个进程都有一个父进程,新的进程不是被全新的创建,通常是从一个原有的进程进行复制或者克隆的。使用 pstree
命令可查看系统中运行的进程之间的关系。
现成的概念早在 20 世纪 60 年代就被提出,但是在操作系统中真正使用多线程是在 20 实际 80 年代中期。在使用线程方面,Soaris 是其中的先驱。在传统的 UNIX 系统中,线程的概念也被使用,但是一个线程对应着一个进程,因此多线程变成了多进程,线程的真正优点没有得到发挥。现在,多线程的技术在操作系统中已经得到普及,被很多操作系统所采用,其中包括 Windows 和 Linux 两大阵营。与传统的进程相比较,用线程来实现相同的功能有如下优点:
Linux 下的多线程遵循 POSIX (Portable Operating System Interface,可移植操作系统接口) 标准,叫作 pthread 。
下面系统过一个简单的例子来一睹多线程的庐山真面目。
#include
#include
#include
static int run = 1; // 运行状态参数
static int retvalue; // 线程返回值
// 线程处理函数
void* start_routine(void* arg) {
int* running = arg;
printf("子线程初始化完毕,传入的参数为:%d\n", *running);
while (*running) {
printf("子线程正在运行\n");
usleep(1);
}
printf("子线程退出\n");
retvalue = 8; // 设置返回值
pthread_exit((void*)&retvalue); // 进程退出并设置退出值
}
int main() {
pthread_t pt;
int ret = -1;
int times = 3;
int i = 0;
int* ret_join = NULL;
ret = pthread_create(&pt, NULL, (void*)start_routine, &run); // 建立线程
if (ret != 0) {
printf("建立线程失败\n");
return 1;
}
usleep(1);
for (i = 0; i < times; ++i) {
printf("主线程打印\n");
usleep(1);
}
run = 0; // 设置线程退出值,让线程退出
pthread_join(pt, (void*)&ret_join); // 等待线程退出
printf("线程返回值为:%d\n", *ret_join);
return 0;
}
/*
编译运行:
jincheng@jincheng-PC:~/Desktop$ gcc -o test test.c -lpthread // 编译时需链接线程库 “-lpthread”
jincheng@jincheng-PC:~/Desktop$ ./test
运行结果:
子线程初始化完毕,传入的参数为:1
子线程正在运行
子线程正在运行
主线程打印
子线程正在运行
子线程正在运行
主线程打印
子线程正在运行
主线程打印
子线程正在运行
子线程退出
线程返回值为:8
*/
在上面的示例中,在一个进程中调用函数 pthread_create()
建立一个子线程,调用函数 pthread_exit()
退出线程。
函数 pthread_create()
用于创建一个某种特性的线程,传入的参数有线程属性、线程函数、线程函数变量。线程中执行线程函数。
其函数原型为:
int pthread_create(pthread_t *thread,
pthread_attr_t *attr,
void* (*start_routine)(void*),
void* arg);
各参数的详细属性为:
pthread_t
类型的变量,其底层类型为 unsigned long int
。**当成功创建线程时,函数返回 0 ;失败时返回非零值。**线程创建成功后,新创建的线程按照参数 3 和 参数 4 确定一个运行函数,原来的线程在线程创建函数返回后继续运行下一行代码。
函数 pthread_join
用于等待一个线程运行结束。这个函数是阻塞函数,一直到被等待的线程结束为止,函数才返回并且收回被等线程的资源。
其函数原型为:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
各参数的详细属性为:
线程函数的结束有两种方式,一种是线程函数运行结束,不用返回结果;另一种是通过函数 pthread_exit()
将结果传出。
其函数原型为:
extern void pthread_exit __P((void *__retval)) __attribute__ ((__noreturn__));
参数 __retval
是函数的返回值,这个值可以被 pthread_join()
函数捕获,通过 __thread_return
参数获得此值。
至此,我们就简单地掌握程序、进程和线程的基本概念了,也了解了多进程、多线程的基本使用方法。关于进程间通信以及线程的更详细的介绍我们后面再慢慢深入,大家先把本次内容消化。忠告:一定要动手敲代码,把示例都运行一遍,在此基础上修改示例,测试自己的新示例。