linux系统编程-基础

26 篇文章 0 订阅

linux系统编程-基础

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
XXXX@ubuntu: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
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值