• 进程


    程序的运行着的实例叫做进程。Unix中,大多数有关进程管理的函数都声明在<unistd.h>头文件中。

    1 进程初步

    进程ID

    Linux中每一个进程都一个独立的ID标识,即pid。进程ID随着进程产生时由系统分配,是一个16位的数字。Linux中,出了特殊的init进程外,每一个进程都有一个父进程。父进程ID叫做ppid。

    C语言中进程ID为pid_t类型,定义在<sys/types.h>文件中。在进程中执行geipid()getppid()可以分别获去当前进程的pid和ppid。

    查看运行中的进程

    Shell中使用ps命令可以查看当前运行的进程。

    $ ps -e -o pid,ppid,command
      
      PID  PPID COMMAND
        1     0 /sbin/launchd
       44     1 /usr/sbin/syslogd
       45     1 /usr/libexec/UserEventAgent (System)
       47     1 /usr/libexec/kextd
      ...
    

    -e选型表示列出所有在运行进程,包括后台系统进程。-o pid,ppid,command表示需要输出进程ID,父进程ID,以及进程启动命令三项。

    2 创建进程

    system函数

    system函数定义在<stdlib.h>文件中。该函数直接调用Shell——/bin/sh来执行命令。如果Shell本身无法调用,system函数返回127值;如果遇到其他错误,就返回-1

    #include <stdlib.h>
    
    int main() {
    	int return_value;
    	return_value = system("ls -l /");
    	return return_value;
    }
    

    system函数较为危险。因为他调用的是/bin/sh。而/bin/sh在不通的Linux中指向不同的的Shell,有些是bash,有些是zsh,也可能碰到tcsh。而且不同的Linux发型版赋予system函数的权限也不完全相同。

    因此,不推荐使用system函数创建进程。

    fork函数和exec族函数

    Linux中,fork函数创建当前进程的一个独立拷贝,exec函数则保证新进程的内容与父进程无关。

    调用fork函数

    程序调用fork函数后会复制当前进程,产生出一个子进程。父进程和子进程都会从“fork”处持续执行代码。两者的仅有的区别就是pid和ppid。

    执行fork函数后会出现两个进程,有不同的返回值。在父进程中,fork函数的返回值是子进程的pid;子进程中,fork函数的返回值是0

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main() 
    {
    	pid_t child_pid;
    	printf("The main process ID is : %d.
    ", (int) getpid()); 
    	
    	child_pid = fork();
    	
    	if (child_pid != 0) {
    		/* parent process */
    		printf("This is a parent process with ID: %d.
    ", (int) getpid());
    		printf("The child process ID is: %D.
    ", (int) child_pid);
    	}else{
    		/* child process */
    		printf("This is a child process with ID: %d.
    ", (int) getpid());
    	}
    	
    	return 0;
    }
    

    调用exec族函数

    exec族函数可以将当前进程中运行的程序替换为其他程序。当程序调用exec族函数后,当前进程立即终止作业,从头执行新程序——根据exec族函数的参数。

    exec族函数是这样的:

    • 名字中有字母“p”的execvpexeclp接受一个程序名称,并在当前执行路径中搜索程序。不含“p”的函数必须给出程序的全部路径。
    • 名字中有字母“v”的execvexecvpexecve可以将一组以NULL结尾的、字符串指针数组作为新程序的参数列表。名字中有字母“l”的execlexeclpexecle函数使用C语言的varargs机制作为参数列表。
    • 名字中有字母“e”的execveexecle可以接受额外的参数——一组环境变量。该数组为以NULL结尾的、字符串指针数组。每一个字符串的形式应为:“VARIABLE=value”。

    除非发生错误,否则exec族函数不会返回任何值。

    同时调用

    通常,先用fork函数,生成一个子进程,然后再子进程中执行exec族函数。这样父进程可以继续当前作业,不受打扰。

    #inlcude <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    int spawn(char* program, char** arg_list)
    {
    	pid_t child_pid;
    	child_pid = fork();
    	
    	if (child_pid != 0) {
    		/* parent process */
    		return child_pid;
    	}else{
    		/* child proces */
    		execvp(program, arg_list);
    		fprintf(stderr, "Error occcured in excevp.
    ");
    		abort();
    	}
    }
    
    int main()
    {
    	char* arg_list = {
    		"ls",
    		"-l",
    		"/",
    		NULL
    	};
    	
    	spawn("ls", arg_list);
    	printf("Done with main program.
    ");
    	
    	return 0;
    }
    

    进程调度

    Linux不能保证父子进程的先后顺序,只能保证每一个都能够得到执行。如果进程间有优先级,那么需要设定niceness参数。

    在Shell中,使用nice程序设定命令的优先级。看例子:

    $ nice -n 10 ls -l /
    

    这条命令将命令ls -l /niceness参数设定为10。niceness数值越高,进程的优先级越低,所能得到的执行时间越少。

    对于正在执行的进程,可以使用renice命令来改变其优先级。

    需要注意的是,只有root用户才可以把niceness参数设定为负值。

    3 信号

    信号是发给进程的特殊消息。信号是异步的:当程序接收到信号,会立即暂停当天作业,改为处理信号。信号有很多种,根据信号代码来区别。信号代码定义在/usr/include/bits/signum.h文件中。C语言中直接引用<signal.h>头文件即可。

    进程接收到信号后,可以根据信号的对策(disposition),做出不同的反应。每一种信号都一个默认对策(default-disposition),如果进程没有选择其他对策,系统就会执行其默认对策。对于大多数信号,进程可以选择忽略,或者调用“信号句柄(Singnal-Handler)”函数来处理。如果要使用信号句柄函数,当前作业就会暂停。当信号句柄函数返回后,当前作业继续执行。

    当进程试图执行非法操作时,Linux会向进程发送诸如SIGBUS(总线错误,bus error),SIGSEGV(段违规,segmentation vialation),SIGFPE(浮点异常,floating point exception)等信号,并试图终止进程。

    使用sigaction函数来设置信号的默认对策。第一个参数是信号代码,后两个是指向sigaction结构的指针:第一个包括信号的默认对策,第二个包含信号以前的对策,(The first of these contains the desired disposition for that signal number, while the second receives the previous disposition)。 sigaction结构中最重要的sa_handler域可以是下面三个中的某一个值:

    • SIG_DFL,定义信号的默认对策;
    • SIG_IGN,定义信号是否可忽略;
    • 一个信号句柄函数指针。该函数只接受一个参数,信号代码,返回void。

    由于信号的处理是异步的,当信号句柄函数处理信号时,主程序处于一个非常脆弱的状态。因此,信号句柄函数中因当避免I/O操作,或对大多数库、系统函数的调用。信号句柄函数因当根据信号执行尽可能少的作业量,然后将控制权返还主程序。主程序会周期性的检查是否有信号到达,并执行相应的作业。尽管不常见,信号句柄函数还是能够因为其他信号而被暂停,注意由此带来的不便。

    即使是修改全局变量也会很危险。试考虑,当全局变量值的修改需要执行两步机器指令时,如果又一个同类信号发生在这两步之间,再次试图对全局变量作出修改,此时前一信号句柄函数执行第二步机器指令时很可能会出错。

    如果信号句柄函数要使用全局变量标记信号,那么这个变量最好是sig_atomic_t类型。Linux保证该类型的修改只需要一条机器指令即可完成。实际上Linux中,sig_atomic_t类型就是int类型。对于int类型、指针类型或者更小的类型,其修改数值的操作都是原子性的。

    #include <signal.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    sig_atomic_t sigusr1_count = 0;
    
    void handler (int signal_num)
    {
    	++sigusr1_count;
    }
    
    int main() 
    {
    	struct sigaction sa;
    	memset(&sa, 0, sizeof(sa));
    	sa.sa_handler = &handler;
    	sigaction(SIGUSR1, &sa, NULL);
    	
    	/* Do some lengthy stuff here. */
    	/* ... */
    	
    	printf(“SIGUSR1 was raised %d times
    ”, sigusr1_count);
    	
    	return 0;
    }
    

    4 进程中止

    进程中止的方法有很多。

    程序可以调用exit函数,也可以在main函数中返回来正常退出。exit函数的参数或者main函数的返回值就是进程的退出代码(exit code)。退出代码的规范可见上一篇笔记《编写出色的GNU/Linux程序》

    进程也会因为收到各类信号而异常退出。例如前面提到的SIGBUS,SIGSEGV,SIGFPE等信号。此外,如果用户按下Ctrl+C,进程会收到SIGINT信号;Shell中的kill命令会发出SIGTERM信号,两者的默认对策都是终止进程。通过调用abort()函数,进程想自己发出SIGABRT信号来中止自身,并生成一个核心文件(core file)。最强大的中止信号是SIGKILL,该信号能够立即无条件中止进程,无法被阻塞或应对(handled)。

    这些信号可以在Shell中用kill命令发出:

    $ kill -KILL <pid>
    

    也可以在程序中用kill函数来发送:

    #include <sys/types.h>
    #include <signal.h>
    
    kill(child_pid, SIGTERM);
    

    等候进程中止

    对于使用fork函数和exec族函数产生子进程的情形来说,通常父进程之行结束后,子进程才会产生结果。

    如果想要父进程等待子进程结束后再执行,可以使用wait族信号。

    wait系统调用

    最简单的函数就是wait,能够阻塞父进程直到其某一子进程退出或出错。该函数通过整数指针参数返回退出代码。WEXITSTATUS宏包含了子进程的退出代码。WIFEXITED宏可以根据子进程退出代码判定进程是正常退出还是被信号中止。对于后者,可以使用WTERMSIG宏来确认是被什么信号中止的。

    使用waitpid函数可以指定等待的进程;wait3函数可以返回进程的CPU使用统计;wait4函数可以对指定等待的进程提供更多选择。

    僵尸进程

    僵尸进程(zombie process)就是一条不再运行但其资源仍然未被回收的进程。考虑父进程fork出子进程后调用了wait函数。如果在父进程调用wait之前,子进程还没中止。那么父进程被阻塞直到子进程中止。如果子进程已经中止了,那么在父进程调用wait之前,该子进程即使一条僵尸进程。父进程调用wait之后,子进程被回收。

    如果父进程结束后,子进程才结束。那么子进程将永远成为僵尸进程。

    异步清除子进程

    如果父进程fork出多条子进程,而且父进程不能被阻塞。那么子进程的回收就成了问题。

    一个解决办法是周期性的调用wait3wait4函数。如果向这两个函数传入WNOHANG,函数就会运行在非阻塞模式:如果有僵尸子进程,回收并返回其pid;如果没有,返回0。

    这里有一个更优雅的解决方案。Linux会在子进程中止时向父进程发送SIGCHLD信号,其默认对策是什么都不做。因此可以修改父进程设定——每次收到SIGCHLD信号就调用一次wait来回收资源。

    #include <signal.h> 
    #include <string.h> 
    #include <sys/types.h> 
    #include <sys/wait.h>
    
    sig_atomic_t child_exit_status;
    
    void clean_up_child_process (int signal_number) {
    	/* Clean up the child process. */
    	int status;
    	wait(&status);
    	/* Store its exit status in a global variable. */ 
    	child_exit_status = status;
    }
    
    int main () {
    	/* Handle SIGCHLD by calling clean_up_child_process. */
    	struct sigaction sigchld_action;
    	memset(&sigchld_action, 0, sizeof (sigchld_action));
    	sigchld_action.sa_handler = &clean_up_child_process;
    	sigaction(SIGCHLD, &sigchld_action, NULL);
    
    	/* Now do things, including forking a child process. */ 
    	/* ... */	
    	
    	return 0;
    }
    
  • 相关阅读:
    sqlserver 动态 sql语句的执行
    SqlServer位运算 权限设计
    更改主数据 的管理员账户
    如何查看dll 的PublicKeyToken
    varbinary 与 字符串 的互换函数
    analysis service 配置远程连接
    sqlserver字符串拆分(split)方法汇总
    openfile 安装备忘
    Lamp 在centos 中的安装
    Oracle查询表中的各列的列名,数据类型,以及类型长度
  • 原文地址:https://www.cnblogs.com/rim99/p/5474468.html
Copyright © 2020-2023  润新知