欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

linux系统编程-1-基础

程序员文章站 2022-06-24 16:54:08
...

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
*/