【Android Linux內存及性能優化】(五) 進程內存的優化 - 線程


本文接着
【Android Linux內存及性能優化】(一) 進程內存的優化 - 堆段
【Android Linux內存及性能優化】(二) 進程內存的優化 - 棧段 - 環境變量 - ELF
【Android Linux內存及性能優化】(三) 進程內存的優化 - ELF執行文件的 數據段-代碼段
【Android Linux內存及性能優化】(四) 進程內存的優化 - 動態庫- 靜態庫


一、內存篇

1.1 系統當前可用內存

1.2 進程的內存使用

1.3 進程內存優化

1.3.1 ELF執行文件

1.3.2 動態庫

1.3.3 靜態庫

1.3.4 線程

在 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;
}

1.3.4.1 設置進程棧空間 ulimit -s

進程和線程的棧大小,是可以在 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 增大棧空間大小即可。


1.3.4.2 設置線程棧空間 pthread_attr_setstacksize

在前面測試,我們知道一個線程的棧內存空間爲2MB,

有些情況下需要調整棧空間的大小,

比如說爲了節省內存,
可以通過將多個進程以爲線程而合併到一個進程,這樣2MB 的線程棧空間可能對由進程來修改而成的線程來說,顯得有點小,(可以節省(N-1)× 6M 的內存)。如果這些線程數據量大的話,那麼每個線程2MB 的空間,顯得有點小,就需要將線程的棧空間擴大。

另外,對於服務器來講,其服務進程有可能會創建數個個線程,那麼每個線程2MB 的空間,又顯得太奢侈了,這時就需要將線程的棧空間減小。


可以使用 pthread_attr_setstacksize 來設置棧空間的大小,設置時一定要慎重,防止過小導致棧溢出。


演示代碼如下:

#include <pthread.h>
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);

1.3.4.3 減少線程數量

一般情況下,一個進程所擁有的線程數量很少,大概在 10個以內,如果每個線程棧使用20kb 的內存,總共消耗200kb 的內存,對系統的影響還是很小的。

可是對於某些網絡服務器的進程,每個用戶請求創建一個線程爲其服務。
如果每個線程的工作時間很長,不能及時退出的話,會導致進程中同時併發大量的線程,這時其線程棧所佔用的內存,就不可以忽視了。比如每個線程20kb,100 個就 2M了,對於內存稀缺的嵌入式設備來講,是個不小的消耗。

對於線程衆多的進程,需要考慮使用異步通信的方式來替代以前的線程+同步通信方式,以達到減少線程的目的。
好處在於: 一方面可以減少內存的使用,另一方面又可以減少線程數量,減輕Linux 在內核做進程調試時的負擔。
缺點在於: 如果使用異步通信方式的話,會帶來編碼的複雜性。


1.3.5 Linux 共享內存

本文主要特指 Linux 共享內存,並不是介紹如何通過編程來實現共享內存,而是共享內存背後的故事。
有關編程方面,可以參考我之前寫的文章《【華爲Hicar倒車影像分流需求 一】- 需求分解 及 進程間通信共享內存原理


1.3.5.1 內核如何支持共享內存的?

進程間需要共享的數據放在一個叫 IPC 共享內存區域的地方,
所有需要訪問該共享內存區域的進程都需要把該共享區域映射到本進程的地址空間中去。

系統共享內存通過 shmget 獲得或創建一個IPC 共享內存區域,關返回相應的標識符。

內核保證shmget 獲得或創建一個共享內存區,初始化該共享內存區域相應的shmid_kernel 結構的同時,還將在特殊文件系統 shm 中,創建並打開一個同名文件,並在內存中建立起該文件相應的dentry 及 inode 結構,新打開的文件不屬於任何一個進程(任何進程都可以訪問該共享內存區)。
所有這一切都是系統調用 shmget 完成的。

在這裏插入圖片描述


在創建 一個共享內存區域後,還要將它映射到進程地址空,系統調用 shmat() 完成此項功能。
由於在調用 shmget() 時,已經創建了文件系統 shm 中的一個同名文件與共享內存區域相對應,因此,調用 shmat() 的過程相當於映射文件系統 shm 中同名文件過程,原理與mmap() 大同小異。


1.3.5.2 共享內存在不同的進程中,是否地址相同?

由於在不同的進程中,讀寫時都要調用 shmat 進行重映射,所以地址不一定相同。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章