Linux C 进程
进程
UNIX编程手册第6 7章完结 24 25 26 27 28
未完待续,可能等到期末考试结束吧
基础知识
进程号是唯一标识进程的正数,数据类型是pid_t
,程序和进程号没有固定关系,init
进程的pid_t
总是1
。
#include <unistd.h>
pid_t getpid(void); // 返回该进程的进程号
pid_t getppid(void); // 返回该进程父进程的进程号
Linux限制进程号不大于32767
(可以使用cat /proc/sys/kernel/pid_max
来查询此Linux内核支持的最大进程号),每一次创建新进程,内核按顺序分配进程号,当进程号到达最大时,内核重置进程号分配器,重置为300
,因为300
内有大量Linux守护进程和系统进程。
内存分布
进程分配进入内存时分成很多段,可以使用size
命令查看二进制文件的文本段,初始化数据段,非初始化数据段。
-
文本段
- 程序的机器语言,只读,可以被多个进程执行
-
初始化数据段
- 显式初始化的全局变量和静态变量
-
未初始化数据段(BBS段)
-
未显式初始化的全局变量和静态变量
-
初始化和未初始化数据段分开存储,这样程序就不需要存储未初始化的数据
因为这些数据可以等到程序加载时再处理,所以不必占用磁盘空间
-
-
栈
-
堆
- 运行时的数据
命令行参数
int main(int argc, char **argv)
命令行参数由Shell解析,argv[0]
是程序本身,argv[argc]
是NULL
。
环境列表
获得环境
全局变量char **environ
存储了环境列表,其组织形式类似argv
。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
char **ep;
for (ep = environ; *ep != NULL; ep++)
puts(*ep);
exit(EXIT_SUCCESS);
}
可以用main函数第三个参数来访问环境列表,因此上面的代码可以这样写
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv, char **envp)
{
char **ep;
for (ep = envp; *ep != NULL; ep++)
puts(*ep);
exit(EXIT_SUCCESS);
}
使用getenv函数检索环境变量中的值,若不存在该环境变量则返回NULL。
#include <stdlib.h>
char *getenv(const char *name)
使用该函数的警示:由于函数返回字符串的指针,所以调用者有能力修改他,这是危险的,但是我的设备上没有影响哎
/**
* 危险的程序
* export say_hello="hello world"
* echo ${say_hello}
**/
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
char *ep = NULL;
ep = getenv("say_hello");
if (ep != NULL)
{
printf("i change the first char with #\n");
ep[0] = '#';
printf("%s\n", getenv("say_hello"));
}
else
{
printf("cound not find\n");
}
exit(EXIT_SUCCESS);
}
/**
* echo ${say_hello} # 仍然输出正确
**/
修改环境
仅仅是临时性的(对于该进程和其子进程有效)修改,如要永久修改,需要对文件操作。
int putenv(char *string);
设置环境变量的指针指向这个string,所以string参数绝对不能是栈中的变量
非本地跳转
goto语句只能实现函数内的跳转,不支持函数间的跳转,尤其是其他函数希望“调用”main函数,不能通过函数的返回来实现(因为返回main后将继续顺序执行而不是从头执行,所以非本地跳转解决的是函数间任意位置的跳转问题(但是这就像goto一样,滥用会导致程序逻辑混乱)。
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
程序逻辑:首先设置setjump函数规定跳转的落地位置和环境,程序自然执行setjmp,此时设置进程环境到env(其中也设置了程序计数寄存器和栈指针寄存器,用于longjmp调用时信息)并返回0,程序调用某一函数,函数中longjmp根据传入的env返回到对应的setjmp中(基于程序计数寄存器和栈指针寄存器),同时设置val用于表明这次跳转的来源。
内存分配
在堆上分配内存malloc()
void *malloc(size_t size);
malloc函数返回的内存块支持大多数硬件架构的对齐方式,通常基于8对齐或者16对齐的,调用malloc(0)
在linux下的行为是创建可以用free释放的内存。调用时若调用成功则增加program break的位置,否则输出时会返回NULL并设置errno。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int *a = NULL;
a = (int*)malloc(0);
if(a == NULL)
{
printf("still null\n");
}
else
{
printf("have a space\n"); // 在linux下输出这个
free(a);
}
return 0;
}
释放内存free()
void free(int *ptr);
free函数仅仅释放空间,并不对指针变量设置为NULL,free不改变program break的位置,只是将这个空闲空间加到内存空闲内存列表中,供后续malloc使用。free(NULL)
不会产生错误,所以这样的代码没有问题(把NULL加到空闲列表不会有错)。但是对一个已经free
却没有设置为NULL
的指针进行二次free
时,free
在尝试将这个内存块放入空闲内存块表中时会出现错误,导致未知问题。
这是一个测试的代码
/**
* 验证free空指针没有问题
* 验证free的调用时先将这个内存定位空
* 再将这个空内存空间放在这个内存表中
* */
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int *a = NULL;
a = (int*)malloc(sizeof(int) * 10);
free(a);
if(a == NULL)
{
printf("a is null\n");
free(a);
printf("free a null is safe\n");
}
else
{
a = NULL;
free(a);
printf("a is null after set and free a null is safe\n");
}
return 0;
}
其他分配内存函数
void *calloc(size_t __nmemb, size_t __size);
void *realloc(void *ptr, size_t size);
后者是对调整内存块的大小,通常是增加,ptr是需要调整空间的指针,size是大小,如果成功,返回调整后的指针(说明这里的指针不止指向内存块的地址,可能还包含内存块大小的信息),否则返回NULL,realloc(prt, 0)
等价于free,realloc(NULL, size)
等价于malloc。
newptr = realloc(prt, newsize);
if(nptr == NULL)
{
/* 处理错误 */
}
else
{
ptr = newptr;
}
内存对齐函数memalign()
分配内存时,起始地址要求的对齐
void *memalign(size_t alignment, size_t size);
alignment指明对齐要求,size指明大小,但alignment等于8时等价于malloc,这个函数用于设备级别,一个havefun代码
/**
* 学习使用memalign函数,探索对齐要求的用法
* 这里按照256对齐,所以地址显示最后两位是00
* */
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <alloca.h>
int main(int argc, char **argv)
{
int i = 0;
char *a = NULL, *b = NULL, *c = NULL;
a = (char*)malloc(21);
b = (char*)memalign(256, 20);
c = (char*)malloc(20);
for(i = 0; i < 5; i++)
printf("%lx\n", (long)&a[i]);
printf("\n");
for(i = 0; i < 5; i++)
printf("%lx\n", (long)&b[i]);
printf("\n");
for(i = 0; i < 5; i++)
printf("%lx\n", (long)&c[i]);
return 0;
}
dwr@ali-s:~/Workspace/C/UNIX manual/7$ ./memalign 564bcd58a2a0
564bcd58a2a1
564bcd58a2a2
564bcd58a2a3
564bcd58a2a4
564bcd58a300
564bcd58a301
564bcd58a302
564bcd58a303
564bcd58a304
564bcd58a410
564bcd58a411
564bcd58a412
564bcd58a413
564bcd58a414
int posix_memalign(void **memptr, size_t alignment, size_t size);
void *alloca(size_t size);
在栈帧上分配内存,和malloc
从堆分配内存不同,该函数不能使用free来释放内存,也不能用*alloc类函数进行二次处理,销毁由他创建的内存只需要函数返回即可。
alloca
优点:
alloca
函数被编译器作为内联函数处理,直接调整栈指针来实现,因此不需要维护空闲内存表,执行速度快于malloc
alloca
函数分配的内存不需要专门释放,随栈指针移除而释放,即函数返回- 在使用longjmp等非局部跳转函数时,
malloc
使用不慎会导致内存泄漏(有时甚至不可避免),但alloca
不会
进程的创建
fork函数
#include <unistd.h>
pid_t fork();
返回
0
:子进程-1
:错误pid
:父进程得到子进程pid
fork函数的工作
- 为子进程分配性内存和内核数据结构,复制程序文本段
- 复制原来的进程上下文(栈段、堆段和数据段)给新进程(意味着对于某些数据是共享的,如文件描述符(内核基于
dup
函数实现)- 子进程继承了父进程的信号处理方式
- 在进程集添加子进程
- fork执行结束,返回给两个进程
- 两进程独立执行,顺序取决于调度
fork函数通常的行文
#include <unistd.h>
int main(int argc, char **argv)
{
pid_t child_pid;
switch (child_pid = fork())
{
case -1:
perror("fork wrong:")
break;
case 0:
/* child action */
break;
default:
/* parent action */
break;
}
return 0;
}
文件的共享
父子进程的文件描述符指向相同的文件,任意进程对文件属性(如偏移量)的修改都会影响到其他进程(举例:相同的标准输出和标准输入)
内存语义
不是简单是复制数据段(因为fork
完后往往会exec
系列函数调用,也就是内核进行的数据复制几乎成了浪费),而是使用小聪明对一些情况进行优化:
- 内核将进程代码段设为只读,
fork
时只是创建页表项指向父进程的物理内存页帧 - 数据段采用写时复制,只有某一进程对共享的内存数据进行写操作时,才会复制。
fork引发的竞争问题
论述了一大堆,结果就是谁先谁后依赖linux版本和/proc/sys/kernel/sched_child_run_first
专有文件设置。
进程的执行
exec系列
int execv(const char*path, const char*arg0, ...);
int execl(const char*path, const char*argv[]);
int execvp(const char*path, const char*arg0, ...);
int execlp(const char*path, const char*argv[]);
- path指明可执行程序文件的路径
- arg指明执行程序的参数
调用成功则没有返回值,否则返回-1。
区别:
- 以v结尾的表示参数作为vector整体传入,以l结尾的表示逐个列举的方式
- 没有p结尾的要求指明可执行文件的绝对路径,以p结尾的可不写绝对路径,因为会优先从$PATH中查找程序
进程的监控
wait系列
pid_t wait(int * status);
阻塞的形式等待一个子进程结束,返回结束的子进程的pid并保存返回的状态,不存在子进程则返回-1
pid_t waitpid(pid_t pid, int * status, int options);
具体的形式等待一个子进程结束,返回结束的子进程的pid并保存返回的状态,不存在子进程则返回-1
-
pid = -1
:等价于wait
,等待任意一个子进程结束 -
pid = > 0
:等待进程ID
为pid
的子进程结束 -
pid = 0
:等待进程组ID
为pid
组中任意一个子进程结束 -
pid < -1
:等待进程组ID
为pid
绝对值中任意一个子进程结束 -
0
阻塞父进程,同wait
-
WNOHANG
-
WUNTRACE
状态监测宏
进程的终止
exit()
#include <unistd.h>
void exit(int status);
将所有资源归还内核,status
表示进程退出状态,可以由父进程wait
函数获得