>


Linux篇:多进程与多线程

29

关键词:Linux , 多进程,多线程

文章写得比较匆忙.....

进程

进程的概念

  • 进程描述是一个程序执行过程,当程序执行后,执行过程开始,则进程产生,执行过程结束,则进程结束。

  • 进程是一个独立的可调度的活动,由操作系统进行统一调度,相应的任务会被调度到CPU中进行执行。

  • 进程一旦产生就需要分配相关资源,同时进程是资源分配的最小单位。

进程与程序的区别

  • 程序是静态的,他是一些保存在磁盘上的指令的有序集合,没有任何执行的概念。

  • 进程是一个动态的概念,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程。

并发与并行

  • 并行执行:表示多个任务能够同时执行,依赖于物理的支持,比如CPU是4核,则可同时执行4个任务。

  • 并发执行:在同一时间段有多个任务在同时执行,由操作系统调度算法来实现,比较典型的就是时间片轮转。

Linux进程管理

  • 在Linux系统中管理进程使用树形管理方式,每个进程都需要与其他某一个进程建立父子关系,对应的进程则叫做父进程。

  • Linux系统会为每个进程分配id,这个id作为当前进程的唯一标识,当进程结束,则会回收。

  • 进程的id与父进程的id分别通过getpid()和getppid()来获取。

进程的地址空间

一旦进程创建后,系统则要为这个进程分配相应的资源,一般系统会为每个进程分配4G的地址空间。

0-3G用户空间:

  • stack:存放非静态的局部变量。

  • heap:动态申请的内存。

  • .bss:未初始化过的全局变量(包括初始化为0的,未初始化过的静态变量)。

  • .data:初始化过并且值不为0的全局变量,初始化过的静态变量(包括初始化为0)。

  • .rodata:只读变量(字符串之类)。

  • .text:程序文本段(包括函数,符号常量)。

当用户需要通过内核获取资源时,会切换到内核态运行,这时当前进程会使用内核空间分配资源。

用户需要切换到内核态运行时,主要通过系统调用。

3G-4G内核空间。

进程的状态管理

进程是动态的过程操作系统内核在管理整个动态过程时会使用状态机,给不同的时间节点设计一个状态,通过状态来确定当前的过程进度。

进程的状态与转换

  • 运行态(TASK_RUNNING):此时进程或正在运行或准备运行。就绪或正在运行都属于运行态。

  • 睡眠态:此时进程在等待一个事件的发生或某种系统资源。

  • 可中断的睡眠(TASK_INTERRUPT):可以被信号唤醒或等待事件或资源就绪。

  • 不可中断的睡眠(TASK_UNTERRUPT):只能等待特定的事件 或资源就绪。

  • 停止态(TASK_STOPPED):进程暂停接受某种处理。

  • 僵尸态(TASK_ZOMBIE):进程已经结束但是还未释放进程资源。

进程相关的命令

ps

ps [options]

  • -A 列出所有进程。

  • -e 与-A功能相似。

  • -w 显示加宽可以显示较多的资讯。

  • -au 显示较详细的信息。

  • -aux 显示所有包含其他使用者的进程。

  • -ef 列出所有进程,相比-aux信息要少。

  • ps -ef | grep "可执行文件名"根据名称查找指定名字。

top

实时显示进程信息。

top [-] [d delay] [q] [c] [S] [s] [i] [n] [b]

  • d:改变显示的更新速度,或是在交谈式指令列按s。

  • q:没有延迟的显示速度,如果使用者是有superuser的权限,则top将会以最高的。

pstree

pstree:将所有进程以树形结构的方式展示 。

kill

kill结束进程或用于显示相关信号

kill [选项] [参数]

选项:一般可以接信号编号

-9:强制终止

进程创建

fork()函数

创建进程的函数需要调用fork()函数,则会产生一个新的进程,调用fork()函数的进程叫做父进程,产生的新进程则为子进程。

功能:创建一个子进程

返回:成功-->返回给父进程的是子进程的pid,返回给子进程的是0;失败-->返回-1,并设置errno

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	pid_t cpid;
	cpid = fork();
	if(cpid == -1)
	{
		perror("[error]fork():");
		return -1;
	}
	printf("hello\n");
	return 0;
}

创建子进程后,与父进程并发执行,子进程从fork()之后开始执行,父子进程的执行顺序由操作系统调度算法决定。

子进程会拷贝父进程的地址空间内容,包括缓冲区、文件描述符等。

进程多任务

父子进程执行不同的任务。使用fork()函数之后,会创建子进程,fork()之后的代码会在父子进程中都执行一遍。如果父子进程执行相同的任务,则正常执行;如果父子进程执行不同的任务,则需要利用fork()函数返回值。

进程的退出

在进程结束时,需要释放进程地址空间以及内核中产生的各种数据结构。资源的释放需要通过调用exit()函数或者_exit()函数来完成。在程序结束时,会自动调用exit(0)函数。

exit函数让当前进程退出,并刷新缓冲区。

exit函数信息如下:

头文件:stdlib.h

函数参数:status 退出状态值。

在系统中定义了两个状态值:

EXIT_SUCCESS--->正常退出 0。

EXIT_FAILURE--->异常退出 1。

_exit不会刷新缓冲区。

进程的等待

wait函数依赖的头文件:

#include<sys/types.h>
#include<sys/wait.h>

在子进程运行结束后,进入僵尸死状态,并释放资源,子进程在内核中的数据结构依然保留。

父进程调用wait()与waitpid()函数等待子进程退出后,释放子进程。

wait()函数

pid_t wait(int *wstatus);

让函数调用者进程进入到睡眠状态,等待子进程进入僵死状态后,释放相关资源并返回。

wstatus:保存子进程退出状态值变量的指针,获取具体值需要使用WAITSTATUS()宏定义。

返回值:

成功 返回退出子进程的pid;

失败 -1。

注意:会阻塞调用者进程(一般为父进程)。

waitpid()函数

与wait()功能一样,但比wait()功能更强大,可以理解成wait()底层调用waitpid()。

pid_t waitpid(pid_t pid, int *wstatus, int options);

pid:进程pid

-1 可以等待任意子进程。

>0 等待id为pid的子进程。

wstatus 保存子进程退出状态值变量的指针。

options:选项。

线程

线程的概念

线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。

一个进程可以有多个线程,多个线程共享同一个进程的所有资源,每个线程参与操作系统的统一调度。

线程资源

共享的资源:

同一块地址空间

文件描述符表

每种信号的处理方式

当前工作目录

用户ID和组ID

独立资源:

线程栈

每个线程都有私有的上下文信息

线程ID

寄存器值

errno变量

信号屏蔽字以及调度优先级

线程命令介绍

在Linux系统中有很多命令可以查看进程,包括pidstat、top、ps,可以查看进程,也可以查看一个进程下的线程。

pidstat

ubuntu需安装sysstat工具后可支持pidstat。

-t :显示指定进程所关联的线程。

-p :指定进程pid。

eg: pidstat -t -p 1234

top

top命令查看某个进程下的线程时,需要用到-H选项结合-p指定pid。

-H:Thread-mode operation

eg:top -H -p 1234

ps

ps -T -p 1234

线程的创建

创建线程调用pthread_creat函数创建子线程。

依赖的头文件:

#include<pthread.h>

函数原型:

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

thread 线程ID变量指针。

attr 线程属性,默认属性可设置。

start_routine 线程执行函数指针。

arg 线程执行函数的参数。

返回值

成功 0。

失败 返回错误码。

eg:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
//线程执行函数,直接由操作系统进行调度执行
void do_thread(void arg)
{
	printf("hellozeng\n");
}
int main(void)
{
	pthread_t tpid = 0;
	int ret;
	ret = pthread_create(&tpid, NULL, do_thread, NULL);
	if(ret != 0)
	{
		fprintf(stderr, "[ERROR] pthread_create():%s\n",strerror(ret));
		exit(EXIT_FAILURE);
	}
	printf("tpid = %ld\n", tpid);
	return 0;
}				

编译:

gcc -lpthread pthread_test.c -o pthread_test

上述程序执行结果只打印了tpid,子线程没执行。

原因:子线程还没来得及执行,主线程已经执行结束,导致所有其他子线程都必须结束,因此要保证主线程不先于子线程结束。

线程退出、等待与分离

线程退出

pthread_exit函数。

依赖的头文件:

#include<pthread.h>

原型:void pthread_exit(void *retval);

retval 获取线程返回值,通过指针传递。

返回值:成功:0 失败 -1

线程等待

主线程需要等待子进程退出,并释放子线程资源,线程等待调用pthread_join函数。

依赖的头文件:

#include<pthread.h>

int pthread_join(pthread_t thread,void **retval)

thread:线程ID。

retval:获取线程退出值的指针。

功能:等待子线程退出,并释放子线程资源。

eg:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
//线程执行函数,直接由操作系统进行调度执行
void do_thread(void arg)
{
	static int a = 10;
	int *b = NULL;
	b = arg;
	printf("hellozeng %d\n",*b);
	pthread_exit((void *)&a);//线程退出
}
int main(void)
{
	pthread_t tpid = 0;
	int ret;
	int *pp = NULL;
	int e = 222;
	ret = pthread_create(&tpid, NULL, do_thread, (void *)&e);
	if(ret != 0)
	{
	fprintf(stderr, "[ERROR] pthread_create():%s\n",strerror(ret));
	exit(EXIT_FAILURE);
	}
	printf("tpid = %ld\n", tpid);
	pthread_join(tpid, (void **)&pp);//等待子线程退出,这里会阻塞主线程
	printf("pp = %d\n",*pp);
	return 0;
}

线程分离

线程分为可结合和可分离线程。

可结合线程

可结合的线程能够被其他线程回收其资源和杀死;在被其他线程回收之前,它的存储器资源是不释放的。线程创建的默认状态是可结合的,可由其他线程调用phtread_join函数等待子进程退出,并释放相关资源。

可分离线程

不能被其他线程回收或杀死,该线程的资源在他终止时由系统来释放,线程分离调用函数:pthread_detach 。

依赖头文件:

#include<pthread.h>

eg:

pthread_detach(tpid);

线程间通讯

进程间通讯方式也适用于线程。

其他:

主线程给子线程传参方式如下:

通过 pthread_create 函数的第 4 个参数 arg 进行传递即可。

eg:主线程传递一个整型变量给子线程

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void do_thread(void arg)
{
	int num = (int )arg;
	printf("num = %d\n",num);
	printf("thread start.\n");
	pthread_exit(NULL);
}
int main(void)
{
	pthread_t tid;
	int err;
	int num = 100;
	err = pthread_create(&tid,NULL,do_thread,&num);
	if(err != 0){
		fprintf(stderr,"[ERROR] pthread_create(): %s \n",strerror(err));
		exit(EXIT_FAILURE);
	}
	pthread_join(tid,NULL);
	return 0;
}

子线程给主线程传参的方式如下:

子线程将需要返回的值存储在 pthread_exit 函数中的 retval 参数中。在主线程中, pthread_join 函数会将线程的返回值 (指针) 保存到 retval 中。

eg:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void do_thread(void arg)
{
	static int retval = 1000;
	pthread_exit((void *)&retval);
}
int main(void)
{
	pthread_t tid;
	int err;
	void *pret = NULL;		 
	err=pthread_create(&tid,NULL,do_thread,NULL);
	if(err != 0){
		fprintf(stderr,"[ERROR] pthread_create(): %s \n",strerror(err));
		exit(EXIT_FAILURE);																																														       
	}	
	pthread_join(tid,&pret);																			
	printf("ret = %d\n",*(int *)pret);
	return 0;
}					

Final

多进程多线程怎么选?

联系比较紧密的任务,在并发时优先选择多线程;任务联系不紧密,比较独立的任务建议选择多进程。

并发方案

多进程方案

优点:

进程空间地址独立,一旦某个进程出现异常,不会影响到其他进程。

缺点:

每个进程都需要分配独立的空间,需要占用更多的内存;进程间协同时,进程间通讯较复杂。

适用:多个任务联系不是很紧密,eg:远程升级

多线程方案

优点:

同一个进程的多个线程可以共享资源,减小内存空间开销。

缺点:

线程没有独立的进程地址空间,一个线程问题会影响到其他线程。

适用:多个任务联系比较紧密时,可使用多线程,eg:处理网络请求。