Linux 进程

From:http://www.cnblogs.com/jacklu/p/5317406.html

From:http://www.cnblogs.com/mickole/p/3188321.html

Linux中fork()函数详解 :http://blog.chinaunix.net/uid-30025978-id-4696748.html


1. 进程控制块(PCB)

在Linux中task_struct结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。
进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。
PCB包含信息:1、进程状态(state);2、进程标识信息(uid、gid);3、定时器(time);4、用户可见寄存器、控制状态寄存器、栈指针等(tss)
 
每个进程都有一个 非负唯一进程ID(PID)。虽然是唯一的,但是PID可以重用,当一个进程终止后,其他进程就可以使用它的PID了。
PID为0的进程为调度进程,该进程是内核的一部分,也称为系统进程;PID为1的进程为init进程,它是一个普通的用户进程,但是以超级用户特权运行;PID为2的进程是页守护进程,负责支持虚拟存储系统的分页操作。
除了PID,每个进程还有一些其他的标识符:




五种进程状态转换如下图所示:


每个进程的task_struct和系统空间堆栈存放位置如下:两个连续的物理页【《Linux内核源代码情景分析》271页】


系统堆栈空间不能动态扩展,在设计内核、驱动程序时要避免函数嵌套太深,同时不宜使用太大太多的局部变量,因为局部变量都是存在堆栈中的。


2. 进程的创建

新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。所谓创建,实际上是“复制”。
 
子进程刚开始,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。即“copy-on-write”。
fork都是由do_fork实现的,do_fork的简化流程如下图:

在Linux系统中,所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个进程。

Linux提供两个函数去处理进程的创建和执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一),PPID(父进程的PID)和某些资源和统计量(例如挂起的信号)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。

fork()使用写时拷贝(copy-on-write)页实现。内核在fork进程时不复制整个进程地址空间,让父进程和子进程共享同一个拷贝,当需要写入时,数据才会被复制,使各进程拥有自己的拷贝。在页根本不会被写入的情况下(fork()后立即exec()),fork的实际开销只有复制父进程的页表以及给子进程创建唯一的task_struct。

创建进程的fork()函数实际上最终是调用clone()函数。创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0);创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES |CLONE_SIGHAND, 0)。

在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。


fork和vfork的区别

fork()与vfock()都是创建一个进程,那他们有什么区别呢?总结有以下三点区别:

  1. fork ():子进程拷贝父进程的数据段,代码段
    vfork ( ):子进程与父进程共享数据段
  2. fork ()父子进程的执行次序不确定
    vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。
  3. vfork ()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。



fork函数


fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。 子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
一个形象的过程:


运行这样一段演示程序:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t pid;
    char *message;
    int n = 0;
    pid = fork();
    while(1)
    {
        if(pid < 0)
    	{
            perror("fork failed\n");
            exit(1);
        }
        else if(pid == 0)
    	{
            n--;
            printf("child's n is:%d\n",n);
        }
        else
    	{
            n++;
            printf("parent's n is:%d\n",n);
        }
        sleep(1);
    }
    exit(0);
}

运行结果:

可以发现子进程和父进程之间并没有对各自的变量产生影响。
一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。


通过for循环创建多个fork

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int n = 5, i;    //默认创建5个子进程
    if(argc == 2)
    {	
        n = atoi(argv[1]);
    }
    for(i = 0; i < n; i++)    //出口1,父进程专用出口
        if(fork() == 0)
            break;    //出口2,子进程出口,i不自增

    if(n == i)
    {
        sleep(n);
        printf("I am parent, pid = %d\n", getpid());
    } 
    else 
    {
        sleep(i);
        printf("I'm %dth child, pid = %d\n", i+1, getpid());
        wait(NULL);
    }
    return 0;
}

代码(多进程):

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

void sys_err(const char *str)
{
    perror(str);
    exit(-1);
}
int main(int argc, char *argv[])
{
    pid_t pid;
    int i, n;

    if (argc < 2) {
        printf("./a.out numchild\n");
        exit(-1);
    }
    n = atoi(argv[1]);
    for (i = 0; i < n; i++) {
        if ((pid = fork()) < 0)
            sys_err("fork");
        else if (pid == 0) 
            break;
    }
    /* 每个子进程中i的值都是不同的,父进程没有保存子进程的id */
    if (pid == 0) {
        /* in child */
        int m = 0;
        //while (m < i) {
        while (1) {
            printf("i = %d\tpid=%d\n", i, getpid());
            sleep(1);
            m++;
        }
    }

    if (pid > 0) {
        /* in parent */
        pid_t cpid;
        while ((cpid = wait(NULL)) > 0)
            printf("child %d is over\n", cpid);
    }

    return 0;
}

多进程拷贝(实现类似 cp 命令):

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

#define N 5

struct child_task {
    char src[256];
    char dest[256];
    int addr;
    int size;
    int num;
};

void sys_err(const char *str)
{
    perror(str);
    exit(-1);
}
int main(int argc, char *argv[])
{
    int nfork, i, task_addr, task_size, total_size, fddest, tmpfd;
    pid_t pid;
    struct stat sbuf;
    int *mem;

    if (argc < 3) {
        printf("./mycp src dest\n");
        exit(-1);
    }
    if (argc == 3)
        nfork = N;
    else 
        nfork = atoi(argv[3]);

    if (stat(argv[1], &sbuf) < 0)
        sys_err("stat");
    total_size = sbuf.st_size;
    fddest = open(argv[2], O_CREAT|O_RDWR, 0644);
    if (fddest < 0)
        sys_err("open");
    if (lseek(fddest, total_size-1, SEEK_SET) < 0)
        sys_err("lseek");
    write(fddest, "\0", 1);
    close(fddest);

    //struct child_task work[nfork];
    struct child_task *work = malloc(nfork * sizeof(struct child_task));
    task_size = total_size / nfork;
    for (i = 0; i < nfork-1; i++) {
        strcpy(work[i].src ,argv[1]);
        strcpy(work[i].dest ,argv[2]);
        work[i].addr = i * task_size;
        work[i].size = task_size;
        work[i].num = i;
    }
    /* 最后一个进程拷贝字节 */
    strcpy(work[i].src ,argv[1]);
    strcpy(work[i].dest ,argv[2]);
    work[i].addr = i * task_size;
    work[i].size = total_size - work[i].addr;
    work[i].num = i;

    tmpfd = open("tmpfile", O_CREAT|O_RDWR|O_EXCL, 0664);
    if (tmpfd < 0)
        sys_err("open tmpfile");

    ftruncate(tmpfd, nfork*sizeof(int));

    mem = mmap(NULL, nfork*sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED, tmpfd, 0);
    if (mem == MAP_FAILED)
        sys_err("mmap");
    memset(mem, 0, nfork*sizeof(int));

    unlink("tmpfile");   //删除临时文件

    for (i = 0; i < nfork; i++) {
        if ((pid = fork()) < 0)
            sys_err("fork");
        else if (pid == 0) {
            /*in child*/
            int fdsrc, fddest, j, len;
            char buf[4096];
            fdsrc = open(work[i].src, O_RDONLY);
            if (fdsrc < 0)
                sys_err("open src");
            fddest = open(work[i].dest, O_WRONLY);
            if (fddest < 0)
                sys_err("open dest");
            if (lseek(fdsrc, work[i].addr, SEEK_SET) < 0)
                sys_err("lseek fdsrc");
            if (lseek(fddest, work[i].addr, SEEK_SET) < 0)
                sys_err("lseek fddest");

            for (j = 0; j < work[i].size; j++) {
                len = read(fdsrc, buf, 1);
                if (len < 0)
                    sys_err("read");
                write(fddest, buf, len);
                mem[work[i].num]++;
                //                usleep(100000);
            }
            close(fdsrc);
            close(fddest);
            break;
        }
    }

    if (pid > 0) {
        pid = fork();
        /* 打印拷贝进程 */
        if (pid == 0) {
            int i, sum = 0;
            while (sum < total_size) {
                sum = 0;
                for (i = 0; i < nfork; i++) {
                    sum += mem[i];
                }
                printf("%d / %d\n", sum, total_size);
                usleep(100000);
            }
        }
        else if (pid > 0) {
            for (i = 0; i < nfork+1; i++)
                wait(NULL);
        }
        else {
            sys_err("fork");
        }
    }

    free(work);
    return 0;
}
myshell 代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>

#define MAXLINE 4096
#define MAXPIPE 16
#define MAXARG 8

struct { 
     char *argv[MAXARG];
     char *in, *out;
} cmd[MAXPIPE+1];

int parse(char *buf, int cmdnum)
{
     int n = 0;
     char *p = buf;

     cmd[cmdnum].in = cmd[cmdnum].out = NULL;
     while (*p != '\0')
     {
          if (*p == ' ')
          {
               *p++ = '\0';
               continue;
          }
          if (*p == '<')
          {
               *p = '\0';
               while (*(++p) == ' ');    /* cat <     file*/
               cmd[cmdnum].in = p;
               if (*p++ == '\0')
                    return -1;
               continue;
          }
          if (*p == '>')
          {
               *p = '\0';
               while (*(++p) == ' ');
               cmd[cmdnum].out = p;
               if (*p++ == '\0')
                    return -1;
               continue;
          }
          if (*p != ' ' && ((p == buf) || *(p-1) == '\0'))
          {
               if (n < MAXARG - 1) {
                    cmd[cmdnum].argv[n++] = p++;   //"ls -l -R > file"
                    continue;
               } else
                    return -1;
          }
          p++;
     }
     if (n == 0)
          return -1;
     cmd[cmdnum].argv[n] = NULL;
     return 0;
}

int main(void)
{
     char buf[MAXLINE];
     pid_t pid;
     int fd, i, j, pfd[MAXPIPE][2], pipe_num, cmd_num;
     char* curcmd, *nextcmd;

     while (1) {
	  printf("mysh%% ");
	  if (!fgets(buf, MAXLINE, stdin))
	       exit(0);
      // "ls -l\n"
	  if (buf[strlen(buf)-1]=='\n')
	       buf[strlen(buf)-1]='\0';
	  cmd_num = 0;
	  nextcmd = buf;
	  while (curcmd = strsep(&nextcmd, "|")) {
	       if (parse(curcmd, cmd_num++)<0) {
		    cmd_num--;
		    break;
	       }
	       if (cmd_num == MAXPIPE + 1)
		    break;
	  }
	  if (!cmd_num)
	       continue;
          pipe_num = cmd_num - 1;

	  for (i=0; i<pipe_num; i++)
	       if (pipe(pfd[i])) {
		    perror("pipe");
		    exit(1);
	       }
	  for (i=0; i<cmd_num; i++)
	       if ((pid=fork())==0)
		    break;

	  if (pid==0) {
	       if (pipe_num) {
		    if (i==0) {
			 dup2(pfd[0][1], STDOUT_FILENO);
			 close(pfd[0][0]);
			 for (j=1; j<pipe_num; j++) {
			      close(pfd[j][0]);
			      close(pfd[j][1]);
			 }
		    } else if (i==pipe_num) {
			 dup2(pfd[i-1][0], STDIN_FILENO);
			 close(pfd[i-1][1]);
			 for (j=0; j<pipe_num-1; j++) {
			      close(pfd[j][0]);
			      close(pfd[j][1]);
			 }
		    } else {
			 dup2(pfd[i-1][0], STDIN_FILENO);
			 close(pfd[i-1][1]);
			 dup2(pfd[i][1], STDOUT_FILENO);
			 close(pfd[i][0]);
			 for (j=0; j<pipe_num; j++) 
			      if (j!=i || j!=i-1) {
				   close(pfd[j][0]);
				   close(pfd[j][1]);
			      }
		    }
	       }
	       if (cmd[i].in) {
		    fd = open(cmd[i].in, O_RDONLY);
		    if (fd != -1)
			 dup2(fd, STDIN_FILENO);
	       }
	       if (cmd[i].out) {
		    fd = open(cmd[i].out, O_WRONLY|O_CREAT|O_TRUNC, 0644);
		    if (fd != -1)
			 dup2(fd, STDOUT_FILENO);
	       }
	       execvp(cmd[i].argv[0], cmd[i].argv);
	       fprintf(stderr, "executing %s error.\n", cmd[i].argv[0]);
	       exit(127);
	  }

		/*  parent */
	  for (i=0; i<pipe_num; i++) {
	       close(pfd[i][0]);
	       close(pfd[i][1]);
	  }
	  for (i=0; i<cmd_num; i++)
	       wait(NULL);
     }
}

父子进程交替数数

/*
 *父子进程交替数数。
*/

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

pid_t pid;

//父进程处理函数
void sig_handler_p(int sig) //parent 
{
	static int i = 1;
	printf("pid = %d, I'm father\t%d\n", getpid(), i);
	sleep(1);
	i += 2;
	//为避免竞态条件出现,应该在捕捉函数内完成数数及信号发送.
	kill(pid, SIGUSR1);     
}

//子进程处理函数
void sig_handler_c(int sig) //child
{
	static int i = 2;
	printf("pid = %d, I am child\t%d\n", getpid(), i);
	sleep(1);
	i += 2;
	kill(getppid(), SIGUSR2);
}

int main(void)
{
	struct sigaction newact_p, oldact_p; //父进程使用
	struct sigaction newact_c, oldact_c; //子进程使用

	//子进程响应SIGUSR1
	newact_c.sa_handler = sig_handler_c;
	sigemptyset(&newact_c.sa_mask);
	newact_c.sa_flags = 0;
	sigaction(SIGUSR1, &newact_c, &oldact_c);

	//父进程响应SIGUSR2
	newact_p.sa_handler = sig_handler_p;
	sigemptyset(&newact_p.sa_mask);
	newact_p.sa_flags = 0;
	newact_p.sa_flags |= SA_RESTART;//多加这一行可以将被信号打断的系统调用重启。
	sigaction(SIGUSR2, &newact_p, &oldact_p);

	//此时父子进程都注册了2个信号处理函数,但都只会收到其中一个信号,
	//子进程只收SIGUSR1,父进程只收SIGUSR2
	pid = fork();
	if (pid == 0) 
	{	
		//子进程
		while (1);
	} 
	else 
	{	
		//父进程
		raise(SIGUSR2);
		if (wait(NULL)) 
		{			
			//wait有可能被信号打断。
			perror("wait error");
		}
		//while (1);//或者不使用wait使用这条语句也可以实现。
	}	
	return 0;
}



调用wait/waitpid

Linux下C语言开发(进程控制编程——wait()、waitpid()):http://blog.csdn.net/thanksgining/article/details/41979699

总结】:当status为NULL时,只要有子进程退出,wait()退出阻塞(且返回值为退出的子进程的进程号),否则一直阻塞直到有子进程退出。当调用wait()函数的进程没有子进程时,返回-1。

wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或该进程接收到一个指定的信号为止。如果该父进程没有子进程或它的子进程已经结束,则wait()就会立即返回。waitpid()的作用和wait()一样,但它并不一定要等待一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的wait()功能,也能支持作用控制。实际上,wait()函数只是waitpid()函数的一个特例,在Linux内部实现wait()函数时直接调用的就是waitpid()函数。


若调用成功则返回清理掉的子进程id,若调用出错则返回-1。

父进程调用wait或 waitpid时可能会:

  1. 阻塞(如果它的所有子进程都还在运行)。

  2. 携带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信 息)。

  3. 出错立即返回(如果它没有任何子进程)。

wait和waitpid函数的区别是:

  • 如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如 果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。

  • wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。

可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进 程终止,起到进程间同步的作用。

如果参数 status不是空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将 status参数指定为 NULL


关于wait 函数

#include <sys/types.h> /* 提供类型pid_t的定义 */ 
#include <sys/wait.h> 
pid_t wait(int *status) 

函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。当然 如果在调用wait()时子进程已经结束,则wait()就会立即返回子进程结束状态值。

注:

  当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.

  wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

  如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态.

  参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:

1,WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)

2, WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。

示例代码 wait.c

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <errno.h>
#include <stdlib.h>
/***********************************************************
    功能说明:进程等待wait()方法的应用
***********************************************************/
void waitprocess();

int main(int argc, char * argv[])
{
    waitprocess();  
}

void waitprocess()
{
    int count = 0;
    pid_t pid = fork();
    int status = -1;
  
    if(pid<0)
    {
        printf("fork error for %m\n",errno );
    }
    else if(pid>0)
    {
        printf("this is parent ,pid = %d\n",getpid() );
        wait(&status);//父进程执行到此,马上阻塞自己,直到有子进程结束。当发现有子进程结束时,就会回收它的资源。    
    }
    else
    {
        printf("this is child , pid = %d , ppid = %d\n",getpid(),getppid() );
        int i;
    
        for (i = 0; i < 10; i++) 
        {
            count++;
            sleep(1);      
            printf("count = %d\n", count)  ;      
        }
        exit(5);    
    }
    printf("child exit status is %d\n", WEXITSTATUS(status));//status是按位存储的状态信息,需要调用相应的宏来还原一下  
    printf("end of program from pid = %d\n",getpid() );   
}

运行结果:



等待所有子进程结束

例如,我fork了10个子进程,返回的pid_t放到一个有10个元素的pid_t数组里面。那么:我如果简单的写一个for循环,来遍历并调用waitpid的话,前一个进程还在运行,后一个进程已经退出了,那么就相当于后一个进程退出以后一直处于僵尸状态,直到我的父进程已经为前一个进程调用过了waitpid。有没有什么好一点的办法能让我比较优雅的wait所有的子进程呢?


父进程循环wait即可,如果没有需要wait的子进程,wait函数会自动返回-1,即结束循环。

while((pid = wait(&stat)) > 0)
{
     printf("child %d terminated\n", pid);
}


请看 waitpid 函数的说明,其中pid这个参数介绍如下:
pid<-1 等待进程组识别码为 pid 绝对值的任何子进程。
pid=-1 等待任何子进程,相当于 wait()。
pid=0 等待进程组识别码与目前进程相同的任何子进程。
pid>0 等待任何子进程识别码为 pid 的子进程。
所以,你可以pid为-1,则waitpid非阻塞等待回收任意子进程。


waitpid使用实例:

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    pid_t pid;
    pid = fork();

    if (pid < 0) 
    {
        perror("fork failed");
        exit(1);
    }

    if (pid == 0) 
    {   //son
        int i;
        for (i = 3; i > 0; i--) 
        {
            printf("This is the child\n");
            sleep(1);
        }
        exit(3);
    } 
    else 
    {   //parent
        int stat_val;
        waitpid(pid, &stat_val, 0); 

        if (WIFEXITED(stat_val))
            printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
        else if (WIFSIGNALED(stat_val))
            printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
    }
    return 0;
}



什么时候使用fork呢?

一个父进程希望子进程同时执行不同的代码段,这在网络服务器中常见——父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。
一个进程要执行一个不同的程序,一般fork之后立即调用exec

vfork函数

vfork与fork对比:
相同:
返回值相同
不同:
fork创建子进程,把父进程数据空间、堆和栈 复制一份;vfork创建子进程,与父进程内存数据 共享
vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行
为什么需要vfork?
因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了,因此vfork就是”为了exec而生“的。

运行这样一段演示程序:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main()
{
    pid_t pid;
    char *message;
    int n = 0;
    int i;
    pid = vfork();
    for(i = 0; i < 10; i++)
    {
        if(pid < 0)
        {
            perror("fork failed\n");
            exit(1);
        }
        else if(pid == 0)
        {
            n--;
            printf("child's n is:%d\n",n);
            if(i == 1) _exit(0);
            //return 0;  
            //exit(0);  
        }
        else
        {
            n++;
            printf("parent's n is:%d\n",n);
        }
        sleep(1);
    }
    exit(0);
}

编译运行后 


可以发现子进程先被执行,exit后,父进程才被执行,同时子进程改变了父进程中的数据

子进程return 0 会发生什么?



从上面我们知道,结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。 如果你在子进程中return,那么基本是下面的过程: 1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。 2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup()) 3)这时,父进程收到子进程exit(),开始从vfork返回,但是尼玛,老子的栈都被你子进程给return干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error) 好了,现在再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装) 可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行
【《vfork挂掉的一个问题》 http://coolshell.cn/articles/12103.html#more-12103

execve

可执行文件装入内核的linux_binprm结构体。
进程调用exec时,该进程执行的程序完全被替换,新的程序从main函数开始执行。因为调用exec并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。
六种不同的exec函数:

当指定filename作为参数时:
如果filename中包含/,则将其视为路径名。
否则,就按系统的PATH环境变量,在它所指定的各个目录中搜索可执行文件。
 
*出于安全方面的考虑,有些人要求在搜索路径中不要包括当前目录。
在这6个函数中,只有execve是内核的系统调用。另外5个只是库函数,他们最终都要调用该系统调用,如下图所示:

execve的实现由do_execve完成,简化的实现过程如下图:



关于这些函数的区别,需要时可以查看《APUE》关于exec函数部分的内容。

运行这样一段演示程序:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

char command[256];
void main()
{
    int rtn; /*child process return value*/
    while(1) 
    {
        printf( ">" );
        fgets( command, 256, stdin );
        command[strlen(command)-1] = 0;
		
        if ( fork() == 0 ) 
        {
            execlp( command, NULL );
            perror( command );
            exit( errno );
        }
        else 
        {
            wait( &rtn );
            printf( " child process return %d\n", rtn );
        }
    }
}

运行结果:




进程终止


正常终止(5种)

从main返回,等效于调用exit
调用exit
exit 首先调用各终止处理程序,然后按需多次调用fclose,关闭所有的打开流。
调用_exit或者_Exit
最后一个线程从其启动例程返回
最后一线程调用pthread_exit


异常终止(3种)

调用abort
接到一个信号并终止
最后一个线程对取消请求作出响应


wait和waitpid函数

wait用于使父进程阻塞,等待子进程退出;waitpid有若干选项,如可以提供一个非阻塞版本的wait,也能实现和wait相同的功能,实际上,linux中wait的实现也是通过调用waitpid实现的。
waitpid返回值:正常返回子进程号;使用WNOHANG且没有子进程退出返回0;调用出错返回-1;
运行如下演示程序

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h> 

int main()
{
    pid_t pid0,pid1;
    pid0 = fork();
    if(pid0 < 0)
    {
        perror("fork");
        exit(1);
    }
    else if(pid0 == 0)
    {
        sleep(5);
        exit(0);//child
    }
    else
    {
        do{
            pid1 = waitpid(pid0,NULL,WNOHANG);
            if(pid1 == 0)
            {
                printf("the child process has not exited.\n");
                sleep(1);
            }
        }while(pid1 == 0);
		
        if(pid1 == pid0)
        {
            printf("get child pid:%d",pid1);
            exit(0);
        }
        else
        {
            exit(1);
        }
    }
    return 0;
}



当把第三个参数WNOHANG改为0时,就不会有上面五个显示语句了,说明父进程阻塞了。



a.out 的代码如下:


#include <stdio.h>
void main()
{
    printf("hello WYJ\n");
}

 

process.c的代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/times.h>
#include <sys/wait.h>

int main()
{
    pid_t pid_1,pid_2,pid_wait;
    pid_1 = fork();
    pid_2 = fork();
	
    if(pid_1 < 0)
    {
        perror("fork1 failed\n");
        exit(1);
    }
    else if(pid_1 == 0 && pid_2 != 0)
    {
        //do not allow child 2 to excute this process.
        if(execlp("./a.out", NULL) < 0)
        {
            perror("exec failed\n");
        }//child;       
        exit(0);
    }
	
    if(pid_2 < 0)
    {
        perror("fork2 failded\n");
        exit(1);
    }
    else if(pid_2 == 0)
    {
        sleep(10);
    }
    if(pid_2 > 0)
    {
        //parent 
        do{
            pid_wait = waitpid(pid_2, NULL, WNOHANG);//no hang
            sleep(2);
            printf("child 2 has not exited\n");
        }while(pid_wait == 0);
		
        if(pid_wait == pid_2)
        {
            printf("child 2 has exited\n");
            exit(0);
        }
        else
        {
            //printf("pid_2:%d\n",pid_2);
            perror("waitpid error\n");
            exit(1);
        }
    }
    exit(0);
}

运行结果:



编写一个多进程程序:该实验有 3 个进程,其中一个为父进程,其余两个是该父进程创建的子进程,其中一个子进程运行“ls -l”指令,另一个子进程在暂停 5s 之后异常退出,父进程并不阻塞自己,并等待子进程的退出信息,待收集到该信息,父进程就返回。


#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
    pid_t child1,child2,child;
    if((child1 = fork()) < 0)
    {
        perror("failed in fork 1");
        exit(1);
    }
    if((child2 = fork()) < 0)
    {
        perror("failed in fork 2");
        exit(1);
    }
    if(child1 == 0)
    {
        //run ls -l
        if(child2 == 0)
        {
            printf("in grandson\n");
        }
        else if(execlp("ls", "ls", "-l", NULL) < 0)
        {
            perror("child1 execlp");
        }
    }
    else if(child2 == 0)
    {
        sleep(5);
        exit(0);
    }
    else
    {
        do{
            sleep(1);
            printf("child2 not exits\n");
            child = waitpid(child2, NULL, WNOHANG);
        }while(child == 0);
    
        if(child == child2)
        {
            printf("get child2\n");
        }
        else
        {
            printf("Error occured\n");
        }
    }
}

运行结果:




init进程成为所有僵尸进程(孤儿进程)的父进程


僵尸进程: 子进程退出,父进程没有回收子进程资源(PCB),则子进程变成僵尸进程。
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为1号进程init进程。


僵尸进程

在进程调用了exit之后,该进程并非马上就消失掉,而是留下了一个成为僵尸进程的数据结构,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。

子进程结束之后为什么会进入僵尸状态? 因为父进程可能会取得子进程的退出状态信息。

如何查看僵尸进程?

linux中命令ps,标记为Z的进程就是僵尸进程。

执行下面一段程序:

#include <sys/types.h>
#include <unistd.h>
int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0)
    {
        printf("error occurred\n");
    }
    else if(pid == 0)
    {
        exit(0);
    }
    else
    {
        sleep(60);
        wait(NULL);
    }
}

运行结果:


ps -ef|grep defunc可以找出僵尸进程

ps -l 可以得到更详细的进程信息

运行结果显示:

运行两次之后发现有两个Z进程,然后等待一分钟后,Z进程被父进程回收。

其中S表示状态:

O:进程正在处理器运行

S:休眠状态

R:等待运行

I:空闲状态

Z:僵尸状态

T:跟踪状态

B:进程正在等待更多的内存分页

C:cpu利用率的估算值

收集僵尸进程的信息,并终结这些僵尸进程,需要我们在父进程中使用waitpid和wait,这两个函数能够手机僵尸进程留下的信息并使进程彻底消失。


守护进程Daemon


1. 守护进程概述

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

守护进程(又叫精灵进程)是linux的后台服务进程。它是一个生存周期较长的进程,没有控制终端,输出无处显示。用户层守护进程的父进程是init进程。

2. 创建守护进程步骤

首先我们要了解一些基本概念:

进程组 :

  • 每个进程也属于一个进程组
  • 每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .
  • 一个进程只能为它自己或子进程设置进程组ID号

会话期:

会话期(session)是一个或多个进程组的集合。

setsid()函数可以建立一个对话期:

 如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期

(1)此进程变成该对话期的首进程

(2)此进程变成一个新进程组的组长进程。

(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误。

(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行


现在我们来给出创建守护进程所需步骤:

编写守护进程的一般步骤步骤:

(1)在父进程中执行fork并exit推出。即 创建子进程,父进程退出,子进程被init自动收养;fork exit

(2)在子进程中调用setsid函数创建新的会话;即 调用setsid创建新会话,成为新会话的首进程,成为新进程组的组长进程,摆脱父进程继承过来的会话、进程组等。

(3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;即改变当前目录为根目录,保证工作的文件目录不被删除;chdir(“/”)

(4)在子进程中调用umask函数,设置进程的umask为0;即 重设文件权限掩码,给子进程更大的权限;umask(0)

(5)在子进程中关闭任何不需要的文件描述符。即 关闭不用的文件描述符,因为会消耗资源;close

说明:

1. 在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续
2. 脱离控制终端,登录会话和进程组
有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid();
说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3. 禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork())
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4. 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
for(i=0;i 关闭打开的文件描述符close(i);>
5. 改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/")
6. 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。


3. 创建守护进程

在创建之前我们先了解setsid()使用:

  #include <unistd.h>

       pid_t setsid(void);

DESCRIPTION
       setsid()  creates a new session if the calling process is not a process
       group leader
The calling process is the leader of  the  new  session,
       the  process group leader of the new process group, and has no control-
       ling tty
The process group ID and session ID of the  calling  process
       are set to the PID of the calling process
The calling process will be
       the only process in this new process group and in this new session
.

//调用进程必须是非当前进程组组长,调用后,产生一个新的会话期,且该会话期中只有一个进程组,且该进程组组长为调用进程,没有控制终端,新产生的group ID 和 session ID 被设置成调用进程的PID

RETURN VALUE
       On success, the (new) session ID of the calling  process  is  returned.
       On  error,  (pid_t) -1  is  returned,  and errno is set to indicate the
       error.

现在根据上述步骤创建一个守护进程:

以下程序是创建一个守护进程,然后利用这个守护进程每个一分钟向daemon.log文件中写入当前时间

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

#define ERR_EXIT(m) \
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while (0);\

void creat_daemon(void);
int main(void)
{
    time_t t;
    int fd;
    creat_daemon();
    while(1){
        fd = open("daemon.log",O_WRONLY|O_CREAT|O_APPEND,0644);
        if(fd == -1)
            ERR_EXIT("open error");
        t = time(0);
        char *buf = asctime(localtime(&t));
        write(fd,buf,strlen(buf));
        close(fd);
        sleep(60);
            
    }
    return 0;
}
void creat_daemon(void)
{
    pid_t pid;
    pid = fork();
    if( pid == -1)
        ERR_EXIT("fork error");
    if(pid > 0 )
        exit(EXIT_SUCCESS);
    if(setsid() == -1)
        ERR_EXIT("SETSID ERROR");
    chdir("/");
    int i;
    for( i = 0; i < 3; ++i)
    {
        close(i);
        open("/dev/null", O_RDWR);
        dup(0);
        dup(0);
    }
    umask(0);
    return;
}
结果:

结果显示:当我一普通用户执行a.out时,进程表中并没有出现新创建的守护进程,但当我以root用户执行时,成功了,并在/目录下创建了daemon.log文件,cat查看后确实每个一分钟写入一次。为什么只能root执行,那是因为当我们创建守护进程时,已经将当前目录切换我/目录,所以当我之后创建daemon.log文件是其实是在/目录下,那肯定不行,因为普通用户没有权限,或许你会问那为啥没报错呢?其实是有出错,只不过我们在创建守护进程时已经将标准输入关闭并重定向到/dev/null,所以看不到错误信息。

一个守护进程的实例:每隔10s写入一个“tick”
#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#define MAXFILE 65535

int main()
{
    int fd,len,i;
    pid_t pid;
    char *buf = "tick\n";
    len = strlen(buf);
    if((pid = fork()) < 0)
    {
        perror("fork failed");
        exit(1);
    }
    else if(pid > 0)
    {
        exit(0);
    }
    setsid();
    if(chdir("/") < 0)
    {
        perror("chdir failed");
        exit(1);
    }
    umask(0);
    for(i = 0; i < MAXFILE; i++)
    {
        close(i);
    }
    while(1)
    {
        if((fd = open("/tmp/dameon.log", O_CREAT | O_WRONLY | O_APPEND, 0600)) < 0)
        {
            perror("open log failed");
            exit(1);
        }
        write(fd, buf, len+1);
        close(fd);
        sleep(10);
    }
}

运行结果:

示例

#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<syslog.h>
#define MAXFILE 65535
 
int main()
{
    int fd,len,i;
    pid_t pid,child;
    char *buf = "tick\n";
    len = strlen(buf);
    if((pid = fork()) < 0)
    {
        perror("fork failed");
        exit(1);
    }
    else if(pid > 0)
    {
        exit(0);
    }
    openlog("Jack", LOG_PID, LOG_DAEMON);
    if(setsid() < 0)
    {
        syslog(LOG_ERR, "%s\n", "setsid");
        exit(1);
    }
    
    if(chdir("/") < 0)
    {
        syslog(LOG_ERR, "%s\n", "chdir");
        exit(1);
    }
    umask(0);
    for(i = 0; i < MAXFILE; i++)
    {
        close(i);
    }
    if((child = fork()) < 0)
    {
        syslog(LOG_ERR, "%s\n", "fork");
        exit(1);
    }
    if(child == 0)
    {
        //printf("in child\n");//can not use terminal from now on.
        syslog(LOG_INFO, "in child");
        sleep(10);
        exit(0);
    }
    else
    {
        waitpid(child, NULL, 0);
        //printf("child exits\n");//can not use terminal from now on.
        syslog(LOG_INFO, "child exits");
        closelog();
        while(1)
        {
            sleep(10);
        }
    }
    
}
真正编写调试的时候会发现需要杀死守护进程。
如何杀死守护进程?
ps -aux  找到对应PID
kill -9 PID


4. 利用库函数daemon()创建守护进程

其实我们完全可以利用daemon()函数创建守护进程,其函数原型:

#include <unistd.h>

int daemon(int nochdir, int noclose);


DESCRIPTION
       The daemon() function is for programs wishing to detach themselves from
       the controlling terminal and run in the background as system daemons.

       If nochdir is zero, daemon()  changes  the  process’s  current  working
       directory to the root directory ("/"); otherwise,

       If  noclose is zero, daemon() redirects standard input, standard output
       and standard error to /dev/null; otherwise,  no  changes  are  made  to
       these file descriptors.

功能:创建一个守护进程

参数:

nochdir:=0将当前目录更改至“/”

noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”

返回值:

成功:0

失败:-1

现在我们利用daemon()改写刚才那个程序:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

#define ERR_EXIT(m) \
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while (0);\

void creat_daemon(void);
int main(void)
{
    time_t t;
    int fd;
    if(daemon(0,0) == -1)
        ERR_EXIT("daemon error");
    while(1){
        fd = open("daemon.log",O_WRONLY|O_CREAT|O_APPEND,0644);
        if(fd == -1)
            ERR_EXIT("open error");
        t = time(0);
        char *buf = asctime(localtime(&t));
        write(fd,buf,strlen(buf));
        close(fd);
        sleep(60);
            
    }
    return 0;
}

结果同刚才一样,也是只有root才能成功,普通用户执行时看不到错误信息
现在让daemon(0,1),就是不关闭标准输入输出结果:


可以看到错误信息
现在让daemon(1,0),就是不重定向,结果如下:


这次普通用户执行成功了,因为没有切换到/目录下,有权限。其实我们可以利用我们刚才写的创建守护进程程序默认daemon()实现:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

#define ERR_EXIT(m) \
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while (0);\

void creat_daemon(int nochdir, int noclose);
int main(void)
{
    time_t t;
    int fd;
    creat_daemon(0,0);
    while(1){
        fd = open("daemon.log",O_WRONLY|O_CREAT|O_APPEND,0644);
        if(fd == -1)
            ERR_EXIT("open error");
        t = time(0);
        char *buf = asctime(localtime(&t));
        write(fd,buf,strlen(buf));
        close(fd);
        sleep(60);
            
    }
    return 0;
}
void creat_daemon(int nochdir, int noclose)
{
    pid_t pid;
    pid = fork();
    if( pid == -1)
        ERR_EXIT("fork error");
    if(pid > 0 )
        exit(EXIT_SUCCESS);
    if(setsid() == -1)
        ERR_EXIT("SETSID ERROR");
    if(nochdir == 0)
        chdir("/");
    if(noclose == 0){
            int i;
    for( i = 0; i < 3; ++i)
    {
        close(i);
        open("/dev/null", O_RDWR);
        dup(0);
        dup(0);
    }

    umask(0);
    return;
}


其他参考资料:
《APUE》
《操作系统》清华大学公开课 向勇、陈渝
《嵌入式Linux应用程序开发详解》
《LInux 的僵尸(zombie)进程》 http://coolshell.cn/articles/656.html





  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值