本文接着
《【Android Linux内存及性能优化】(一) 进程内存的优化 - 堆段》
《【Android Linux内存及性能优化】(二) 进程内存的优化 - 栈段 - 环境变量 - ELF》
《【Android Linux内存及性能优化】(三) 进程内存的优化 - ELF执行文件的 数据段-代码段》
《【Android Linux内存及性能优化】(四) 进程内存的优化 - 动态库- 静态库》
在 Linux 中,输入 ps -ef
可以列出当前所有的进程:
ciellee@sh:~$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 11:33 ? 00:00:02 /sbin/init splash
root 2 0 0 11:33 ? 00:00:00 [kthreadd]
root 4 2 0 11:33 ? 00:00:00 [kworker/0:0H]
root 6 2 0 11:33 ? 00:00:00 [mm_percpu_wq]
root 7 2 0 11:33 ? 00:00:01 [ksoftirqd/0]
ciellee 2893 1620 0 11:35 ? 00:02:09 /usr/share/code/code --no-sandbox --unity-launch
ciellee 2902 2893 0 11:35 ? 00:00:00 /usr/share/code/code --type=zygote --no-sandbox
ciellee 2936 2893 1 11:35 ? 00:03:44 /usr/share/code/code --type=gpu-process --field-trial-ha
ciellee 2946 2893 0 11:35 ? 00:00:00 /usr/share/code/code --type=utility --field-trial-handle
ciellee 3055 2893 0 11:35 ? 00:00:10 /usr/share/code/code --type=renderer --disable-color-cor
可以发现很多具有同样的名字的进程,PPID 一样,但PID 确不一样,
以 PID=2902 PPID = 2893
的 vscode 工具为便,我当前电脑正在运行 vscode。
可通过命令 cat /proc/2902/status
查看该进程相关信息,
其中 PID 是当前进程号, PPID 是父进程号, Threads 可以看出当前进程中含有多少个线程。
ciellee@sh:~$ cat /proc/2893/status
Name: code
Umask: 0002
State: S (sleeping)
Tgid: 2893
Ngid: 0
Pid: 2893 ---------> 子进程 PID
PPid: 1620 ---------> 父进程 PID
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
FDSize: 128
Groups: 4 24 27 30 46 113 128 1000
NStgid: 2893
NSpid: 2893
NSpgid: 2452
NSsid: 2452
VmPeak: 893776 kB
VmSize: 881132 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 152904 kB
VmRSS: 120664 kB
RssAnon: 47724 kB
RssFile: 72940 kB
RssShmem: 0 kB
VmData: 325992 kB
VmStk: 136 kB
VmExe: 106284 kB
VmLib: 41724 kB
VmPTE: 1480 kB
VmSwap: 0 kB
HugetlbPages: 0 kB
CoreDumping: 0
Threads: 27 ---------> 当前进程有27个线程
SigQ: 4/63166
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 00000001880056fb
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
Speculation_Store_Bypass: thread vulnerable
Cpus_allowed: ff
Cpus_allowed_list: 0-7
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 435153
nonvoluntary_ctxt_switches: 4675
ciellee@sh:~/work/code/CN300S_8950_0525$
接下来进入我们的主角,线程。
线程中的代码段、数据段、堆段 以及 栈段 又是如何呢?
在LInux 中,线程有两种方式, 一种是 pthread,另一种是 NPTL。
本文针对 pthread 分析下。
从编程的角度来讲,线程与进程的重要区别就是进程拥有自已的地址空间,而线程则是完全共享的。
从这个角度来讲,进程和动态库的数据段在线程之间是完全共享的,任何一个线程都可以访问到进程的全局变量。
堆段也是在进程内共享的,一个线程中申请到的一块内存,在另一个线程也能访问。
每个线程的栈段都是私有的,不可共享,因为每个线程都要有自已的运行过程。
在进程中,每创建一个线程,新创建的线程将调用 mmap在虚拟内存顶部分配一个 2MB 的虚拟内存,并且使用一个页面做隔离保护,以此作为线程的栈空间。
当线程执行完毕退出后,每个线程分配的栈空间依然存在,并没有释放,只有当调用 phread_join
进行线程同步后,才会释放。
头文件 : #include <pthread.h>
函数定义: int pthread_join(pthread_t thread, void **retval);
描述 :
pthread_join()函数,以阻塞的方式等待thread指定的线程结束。
当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。
并且thread指定的线程必须是joinable的。
示例如下:
int main(int argc, char *argv[])
{
int first = 0;
int i = 0;
void * ret = NULL;
pthread_t tid[N] = {0};
printf("first=%p\n", &first);
for( i=0; i<N; i++){
pthread_create(tid+i, NULL, thread_proc, NULL);
}
for( i=0; i<N; i++){
pthread_join(tid[i], &ret);
}
pause();
return 0;
}
进程和线程的栈大小,是可以在 Linux 内核中设置的。
使用 ulimit 命令,可以查看和设置一个进程的栈空间的大小。
ciellee@sh:~$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 63166
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192 // 进程栈空间
cpu time (seconds, -t) unlimited
max user processes (-u) 63166
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
可以看出,在Linux 中,默认栈空间大小为 8M。
当进程使用栈大于8M 时,运行时会报 Segmentation fault(core dumped)
错误,此时增通过 ulimit -s 16384
增大栈空间大小即可。
在前面测试,我们知道一个线程的栈内存空间为2MB,
有些情况下需要调整栈空间的大小,
比如说为了节省内存,
可以通过将多个进程以为线程而合并到一个进程,这样2MB 的线程栈空间可能对由进程来修改而成的线程来说,显得有点小,(可以节省(N-1)× 6M 的内存)。如果这些线程数据量大的话,那么每个线程2MB 的空间,显得有点小,就需要将线程的栈空间扩大。
另外,对于服务器来讲,其服务进程有可能会创建数个个线程,那么每个线程2MB 的空间,又显得太奢侈了,这时就需要将线程的栈空间减小。
可以使用 pthread_attr_setstacksize
来设置栈空间的大小,设置时一定要慎重,防止过小导致栈溢出。
演示代码如下:
#include
pthread_attr_t tattr;
pthread_t tid;
int ret;
size_t size = PTHREAD_STACK_MIN + 0x4000;
//initialized with default attributes
ret = pthread_attr_init(&tattr);
// setting the size of the stack also
ret = pthread_attr_setstacksize(&tattr, sieze);
// only size specified in tattr
ret = pthread_create(&tid, &tattr, start_routine, arg);
一般情况下,一个进程所拥有的线程数量很少,大概在 10个以内,如果每个线程栈使用20kb 的内存,总共消耗200kb 的内存,对系统的影响还是很小的。
可是对于某些网络服务器的进程,每个用户请求创建一个线程为其服务。
如果每个线程的工作时间很长,不能及时退出的话,会导致进程中同时并发大量的线程,这时其线程栈所占用的内存,就不可以忽视了。比如每个线程20kb,100 个就 2M了,对于内存稀缺的嵌入式设备来讲,是个不小的消耗。
对于线程众多的进程,需要考虑使用异步通信的方式来替代以前的线程+同步通信方式,以达到减少线程的目的。
好处在于: 一方面可以减少内存的使用,另一方面又可以减少线程数量,减轻Linux 在内核做进程调试时的负担。
缺点在于: 如果使用异步通信方式的话,会带来编码的复杂性。
本文主要特指 Linux 共享内存,并不是介绍如何通过编程来实现共享内存,而是共享内存背后的故事。
有关编程方面,可以参考我之前写的文章《【华为Hicar倒车影像分流需求 一】- 需求分解 及 进程间通信共享内存原理》
进程间需要共享的数据放在一个叫 IPC 共享内存区域的地方,
所有需要访问该共享内存区域的进程都需要把该共享区域映射到本进程的地址空间中去。
系统共享内存通过 shmget 获得或创建一个IPC 共享内存区域,关返回相应的标识符。
内核保证shmget 获得或创建一个共享内存区,初始化该共享内存区域相应的shmid_kernel 结构的同时,还将在特殊文件系统 shm 中,创建并打开一个同名文件,并在内存中建立起该文件相应的dentry 及 inode 结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。
所有这一切都是系统调用 shmget 完成的。
在创建 一个共享内存区域后,还要将它映射到进程地址空,系统调用 shmat() 完成此项功能。
由于在调用 shmget() 时,已经创建了文件系统 shm 中的一个同名文件与共享内存区域相对应,因此,调用 shmat() 的过程相当于映射文件系统 shm 中同名文件过程,原理与mmap() 大同小异。
由于在不同的进程中,读写时都要调用 shmat 进行重映射,所以地址不一定相同。