linux系统编程-1-基础
linux系统编程-1-基础
1. 基础
进程
0-3G用户空间需要映射到各自的物理内存;不同进程的3-4G内核空间映射到同一物理内存。
每个进程在内核(3-4G)中都有一个PCB来维护进程相关的信息,Linux内核的进程控制块是task_struct
结构体,定义于/usr/src/linux-headers-3.16.0-30/include/linux/sched.h
,可以grep -r "task_struct{" /usr/include
查看。
重要的内部成员有:
pid_t
- 进程的状态(就绪、运行、挂起、停止等)
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit),
ulimit -a
可以查看系统资源限制。
环境变量
存储形式:与命令行参数类似。char *environ[]
数组,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:须声明环境变量,extern char ** environ;
常见环境变量:
-
PATH
,可执行文件的搜索路径; -
SHELL
,通常是/bin/bash
; -
TERM
,当前终端类型,在图形界面终端下它的值通常是xterm; -
LANG
,决定了字符编码以及时间、货币等信息的显示格式; -
HOME
,当前用户主目录的路径
获取环境变量值,char *getenv(const char *name);
设置环境变量,int setenv(const char *name, const char *value, int overwrite);
成功0,失败-1;参数overwrite取值: 1覆盖原环境变量 ,0不覆盖(该参数常用于设置新环境变量)。
删除环境变量,int unsetenv(const char *name);
,成功0,失败-1 。name不存在仍返回0,但name为"ABC="
时则会出错。
进程控制
大名鼎鼎的pid_t fork(void);
不赘述了。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid = 0;
pid = fork();
puts("xxxxxxxxxxxx");
if(pid < 0)
{
perror("fork() error");
exit(1);
}
else if(pid > 0)
{
printf("I'm parent,pid: %u, ppid:%u\n",getpid(),getppid());
sleep(1);
}
else
{
printf("I'm child, pid: %u, ppid:%u\n",getpid(),getppid());
}
puts("end~~~~~");
return 0;
}
/*
xxxxxxxxxxxx
I'm parent,pid: 3447, ppid:3040
xxxxxxxxxxxx
I'm child, pid: 3448, ppid:3447
end~~~~~
end~~~~~
*/
sleep()
可以保证子进程不会成为孤儿进程,否则子进程的父进程会变为init(pid==1)
,并且bash会获得cpu,导致输出混乱。
I'm parent,pid: 3447, ppid:3040
[email protected]:xxxxxxxxxxxx
I'm child, pid: 3448, ppid:3447
以下是循环创建3个进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid = 0;
int i = 0;
puts("xxxxxxxxxxxx");
for(i = 0; i < 3; i++)
{
pid = fork();
if(pid < 0)
{
puts("pid < 0");
}
else if(pid == 0)
{
break;
}
}
if(pid < 3)
{
printf("I'm child %d,pid:%u\n",i,pid);
}
else
{
sleep(i);
printf("I'm parent %d,pid:%u\n",i,pid);
}
puts("end~~~~~");
return 0;
}
/*
xxxxxxxxxxxx
I'm child 2,pid:0
end~~~~~
I'm child 1,pid:0
end~~~~~
I'm child 0,pid:0
end~~~~~
I'm parent 3,pid:3699
end~~~~~
*/
fork()
实际上是复制了父进程的0-3G的部分用户空间,以及PCB(pid不同)。父子进程间遵循读时共享写时复制(COW)的原则,不需要完全复制3G空间。
相同部分包括:全局变量,.data, .bss…, 堆栈,环境变量,用户id,宿主目录,信号处理方式…
不同的有:pid,ppid,fork返回值,进程运行时间,定时器,未决信号集。
注意,全局变量等,相同,但各自独立,不共享。
父子进程共享:
-
文件描述符(打开文件的结构体)
-
mmap建立的映射区 (进程间通信详解)
父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
gdb
gcc 1.c -g
,然后gdb a.out
。
list
显示代码。
run
运行,start
单步运行。
gdb只能跟踪一个进程,默认跟踪父进程。可以在fork函数调用之前,通过指令设置。
-
set follow-fork-mode child
命令设置gdb在fork之后跟踪子进程。 -
set follow-fork-mode parent
设置跟踪父进程。
n
步过。
2. exec函数族
fork创建子进程后,子进程往往要调用一种exec函数执行和父进程相同的程序。
当进程调用一种exec函数时,当前进程的.text、.data替换为所要加载的程序的.text、.data,从新程序的启动例程(调用main)开始执行,调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
该族函数执行成功不返回,执行失败返回-1.
man exec
int execl(const char *path, const char *arg, ...); //通过 路径+程序名 来加载。
int execlp(const char *file, const char *arg, ...); //list path,借助PATH环境变量,通常用来调用系统程序,如ls
int execle(const char *path, const char *arg, ..., char *const envp[]); //list environment
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
重点掌握前两个。
execlp("ls","ls","-l",NULL);
,第二个参数为args[0]
,并不使用,随便写也不会报错。
该族的函数参数一定要以NULL
结束。
execv()
这样使用:
char *argv = {"ls","-l",NULL};
execv("/bin/ls",argv);
3. dup2
2:to 4:for
int dup2(int oldfd, int newfd);
把oldfd复制给newfd。
如果fd==3,指向一个文件,dup2(2,3);
,则3指向了stderr
。
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd = 0;
fd = open("out",O_WRONLY|O_CREAT|O_TRUNC,0644);
dup2(fd,STDOUT_FILENO);
execlp("ls","ls","-l",NULL);
close(fd);
return 0;
}
4. wait()
下面是一个产生僵尸进程的例子。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
pid = fork();
if (pid == 0) {
printf("---child, my parent= %d, going to sleep 10s\n", getppid());
sleep(10);
printf("-------------child die--------------\n");
} else if (pid > 0) {
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
子进程终止后,残留资源(PCB)存放于内核中,没有被父进程回收,变成了僵尸进程。
ps aux | grep zoom
,查看。
starr 4623 0.0 0.0 0 0 pts/0 Z+ 11:33 0:00 [zoom] <defunct>
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态(Shell中用特殊变量$?查看),如果是异常终止则保存着导致该进程终止的信号是哪个。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)。
pid_t wait(int *status);
成功则清理掉的子进程ID,失败则返回-1 (没有子进程)。
可以retpid = wait(NULL);
,也可以retpid = wait(&retstatus);
,获取状态,借助宏函数来进一步判断进程终止的具体原因。常用宏函数有以下2组:
-
WIFEXITED(status)
为非0,则 进程正常结束WEXITSTATUS(status)
如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数) -
WIFSIGNALED(status)
为非0,则进程异常终止WTERMSIG(status)
如上宏为真,使用此宏,可取得使进程终止的那个信号的编号。
其他宏函数可以man 2 wait
查询。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
int status;
pid = fork();
if (pid == 0) {
printf("---child, my parent= %d, going to sleep 20s\n", getppid());
sleep(20);
printf("-------------child die--------------\n");
exit(77);
} else if (pid > 0) {
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
wpid = wait(&status);
if (wpid == -1) {
perror("wait error");
exit(1);
}
if (WIFEXITED(status)) { //为真说明子进程正常结束
printf("child exit with %d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) { //为真说明子进程被信号终止(异常)
printf("child is killed by %d\n", WTERMSIG(status));
}
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
运行后使用kill
发送信号测试WTERMSIG(status)
。
如果要回收多个子进程,可以在父进程里添加while(wait(NULL));
waitpid()
waitpid()
可以指定子进程回收,返回值同wait()
。
pid_t waitpid(pid_t pid, int *status, in options);
成功则返回清理掉的子进程ID;失败则返回-1(无子进程)。
参3为WNOHANG,且子进程正在运行时,返回0。
第三个参数可以设置阻塞状态,0为阻塞,WNOHANG
为非阻塞回收,其它查看手册。
关于第一个参数:
- 大于0: 回收指定ID的子进程
- -1 :回收任意子进程(相当于wait)
- 0 :回收和当前调用waitpid一个组的所有子进程
- 小于-1: 回收指定进程组内的任意子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int n = 5, i;
pid_t p, q;
if(argc == 2){
n = atoi(argv[1]);
}
q = getpid();
for(i = 0; i < n; i++) {
p = fork();
if(p == 0) {
break;
}
}
if(n == i){ // parent
sleep(n);
printf("I am parent, pid = %d\n", getpid());
for (i = 0; i < n; i++) {
p = waitpid(0, NULL, WNOHANG);
printf("wait pid = %d\n", p);
}
} else {
sleep(i);
printf("I'm child %d, pid = %d\n",
i+1, getpid());
}
return 0;
}
/*
I'm child 1, pid = 5464
I'm child 2, pid = 5465
I'm child 3, pid = 5466
I'm child 4, pid = 5467
I'm child 5, pid = 5468
I am parent, pid = 5463
wait pid = 5464
wait pid = 5465
wait pid = 5466
wait pid = 5467
wait pid = 5468
*/