linux進(jìn)程

1 進(jìn)程介紹
  1.1 進(jìn)程和程序
  1.2 進(jìn)程層次結(jié)構(gòu)
  1.3 進(jìn)程狀態(tài)
2 進(jìn)程控制塊
  2.1 進(jìn)程狀態(tài)
  2.2 進(jìn)程標(biāo)志符
  2.3 進(jìn)程之間親屬關(guān)系
3 進(jìn)程創(chuàng)建與調(diào)度
  3.1 進(jìn)程創(chuàng)建
  3.2 進(jìn)程調(diào)度
4. 進(jìn)程間通信
  4.1 管道
    4.1.1 匿名管道pipe
    4.1.2 有名管道fifo
  4.2 信號(hào)
  4.3 消息隊(duì)列
  4.4 共享內(nèi)存
  4.5 信號(hào)量
  4.6 套接字
5 進(jìn)程相關(guān)命令
  5.1 ps/pstree
  5.2 kill/killall
  5.3 ipcs/ipcrm
6. 其他
  6.1 進(jìn)程/線程context switch消耗
7 參考

1 進(jìn)程介紹

1.1 進(jìn)程和程序

所謂進(jìn)程是由正文段(text)、用戶數(shù)據(jù)段(user segment)以及系統(tǒng)數(shù)據(jù)段(system segment)共同組成的一個(gè)執(zhí)行環(huán)境, 具體如下圖所示:

進(jìn)程的組成

1.2 進(jìn)程層次結(jié)構(gòu)

Linux所有進(jìn)程形成了一顆完整的進(jìn)程樹,Linux在啟動(dòng)時(shí)就創(chuàng)建一個(gè)稱為init的特殊進(jìn)程,顧名思義,它是起始進(jìn)程,是祖先,以后誕生的所有進(jìn)程都是它的后代——或是它的兒子,或是它的孫子。init進(jìn)程為每個(gè)終端(tty)創(chuàng)建一個(gè)新的管理進(jìn)程,這些進(jìn)程在終端上等待著用戶的登錄。當(dāng)用戶正確登錄后,系統(tǒng)再為每一個(gè)用戶啟動(dòng)一個(gè)shell進(jìn)程,由shell進(jìn)程等待并接受用戶輸入的命令信息,下圖是一顆進(jìn)程樹示意圖:

進(jìn)程樹

通過 pstree 命令或者 ps -ejH 命令可以查看Linux進(jìn)程樹。
  如果子進(jìn)程所屬的父進(jìn)程結(jié)束了,這個(gè)子進(jìn)程就會(huì)變成"孤兒進(jìn)程",init進(jìn)程會(huì)負(fù)責(zé)收養(yǎng)孤兒進(jìn)程。
  如果子進(jìn)程退出的信號(hào)父進(jìn)程沒有通過wait或者waitpid處理,那么子進(jìn)程會(huì)釋放基本所有占用的資源,除了保留PCB(見后文)之外,這樣的進(jìn)程會(huì)變成"僵尸進(jìn)程"

2 進(jìn)程控制塊

Linux進(jìn)程控制塊PCB描述了進(jìn)程的狀態(tài)、優(yōu)先級(jí)、地址空間以及可訪問的文件等等信息,Linux上使用了一個(gè)task_struct結(jié)構(gòu)體描述了這些信息,這個(gè)結(jié)構(gòu)體包含的詳細(xì)信息如下:

  1. 狀態(tài)信息:描述進(jìn)程動(dòng)態(tài)的變化,如就緒態(tài),等待態(tài),僵死態(tài)等
  2. 鏈接信息: 描述進(jìn)程的親屬關(guān)系,如祖父進(jìn)程,父進(jìn)程,養(yǎng)父進(jìn)程,子進(jìn)程,兄進(jìn)程,孫進(jìn)程等
  3. 各種標(biāo)識(shí)符:用簡單數(shù)字對進(jìn)程進(jìn)行標(biāo)識(shí),如進(jìn)程標(biāo)識(shí)符,用戶標(biāo)識(shí)符等
  4. 進(jìn)程間通信信息:描述多個(gè)進(jìn)程在同一任務(wù)上協(xié)作工作,如管道,消息隊(duì)列,共享內(nèi)存,套接字等
  5. 時(shí)間和定時(shí)器信息:描述進(jìn)程在生存周期內(nèi)使用CPU時(shí)間的統(tǒng)計(jì)、計(jì)費(fèi)等信息等
  6. 調(diào)度信息:描述進(jìn)程優(yōu)先級(jí)、調(diào)度策略等信息
  7. 文件系統(tǒng)信息:對進(jìn)程使用文件情況進(jìn)行記錄,如文件描述符,系統(tǒng)打開文件表,用戶打開文件表等
  8. 虛擬內(nèi)存信息:描述每個(gè)進(jìn)程擁有的地址空間,也就是進(jìn)程編譯連接后形成的空間
  9. 處理器環(huán)境信息:描述進(jìn)程的執(zhí)行環(huán)境(處理器的各種寄存器及堆棧等)

在進(jìn)程的整個(gè)生命周期中,系統(tǒng)(也就是內(nèi)核)總是通過PCB對進(jìn)程進(jìn)行控制的;當(dāng)系統(tǒng)創(chuàng)建一個(gè)新的進(jìn)程時(shí),就為它建立一個(gè)PCB;進(jìn)程結(jié)束時(shí)又收回其PCB,進(jìn)程隨之也消亡。PCB是內(nèi)核中被頻繁讀寫的數(shù)據(jù)結(jié)構(gòu),故應(yīng)常駐內(nèi)存。

2.1 進(jìn)程狀態(tài)

Linux上進(jìn)程狀態(tài)如下描述:

  • 就緒態(tài)(TASK_RUNNING):正在運(yùn)行或準(zhǔn)備運(yùn)行,處于這個(gè)狀態(tài)的所有進(jìn)程組成就緒隊(duì)列
  • 睡眠(或等待)態(tài):分為淺度睡眠態(tài)和深度睡眠態(tài)
  • 淺度睡眠態(tài)(TASK_INTERRUPTIBLE):進(jìn)程正在睡眠(被阻塞),等待資源有效時(shí)被喚醒,不僅如此,也可以由其他進(jìn)程通過信號(hào) 或時(shí)鐘中斷喚醒
  • 深度睡眠態(tài)(TASK_UNINTERRUPTIBLE): 與前一個(gè)狀態(tài)類似,但其它進(jìn)程發(fā)來的信號(hào)和時(shí)鐘中斷并不能打斷它的熟睡,處于uninterruptible sleep狀態(tài)的進(jìn)程通常是在等待IO,比如磁盤IO,網(wǎng)絡(luò)IO,其他外設(shè)IO,如果進(jìn)程正在等待的IO在較長的時(shí)間內(nèi)都沒有響應(yīng), 在ps命令看到的是處于-D狀態(tài)的進(jìn)程,除非等待的事件響應(yīng),否則只能重啟系統(tǒng)才能干掉這個(gè)進(jìn)程
  • 暫停狀態(tài)(TASK_STOPPED):進(jìn)程暫停執(zhí)行,比如,當(dāng)進(jìn)程接收到如下信號(hào)后,進(jìn)入暫停狀態(tài):
    SIGSTOP-停止進(jìn)程執(zhí)行
    SIGTSTP-從終端發(fā)來信號(hào)停止進(jìn)程
    SIGTTIN-來自鍵盤的中斷
    SIGTTOU-后臺(tái)進(jìn)程請求輸出
  • 僵死狀態(tài)(TASK_ZOMBIE):進(jìn)程執(zhí)行結(jié)束但尚未消亡的一種狀態(tài)。此時(shí),進(jìn)程已經(jīng)結(jié)束且釋放大部分資源,但尚未釋放其PCB
Linux進(jìn)程狀態(tài)

其中就緒狀態(tài)表示進(jìn)程已經(jīng)分配到除CPU以外的資源,等CPU調(diào)度它時(shí)就可以馬上執(zhí)行了。運(yùn)行狀態(tài)就是正在運(yùn)行了,獲得包括CPU在內(nèi)的所有資源。等待狀態(tài)表示因等待某個(gè)事件而沒有被執(zhí)行,這時(shí)候不耗CPU時(shí)間,而這個(gè)時(shí)間有可能是等待IO、申請不到足夠的緩沖區(qū)或者在等待信號(hào)。

2.2 進(jìn)程標(biāo)志符

每個(gè)進(jìn)程有進(jìn)程標(biāo)識(shí)符、用戶標(biāo)識(shí)符、組標(biāo)識(shí)符。進(jìn)程標(biāo)識(shí)符PID是32位的無符號(hào)整數(shù),它被順序編號(hào):新創(chuàng)建進(jìn)程的PID通常是前一個(gè)進(jìn)程的PID加1。在Linux上允許的最大PID號(hào)是由變量pid_max來指定,可以在內(nèi)核編譯的配置界面里配置0x1000和0x8000兩種值,即在4096以內(nèi)或是32768以內(nèi)。當(dāng)內(nèi)核在系統(tǒng)中創(chuàng)建進(jìn)程的PID大于這個(gè)值時(shí),就必須重新開始使用已閑置的PID號(hào),可以通過cat命令查看系統(tǒng)pid_max的值。

cat /proc/sys/kernel/pid_max
32768

另外,每個(gè)進(jìn)程都屬于某個(gè)用戶組。task_struct結(jié)構(gòu)中定義有用戶標(biāo)識(shí)符UID(User Identifier)和組標(biāo)識(shí)符GID(Group Identifier)。它們同樣是簡單的數(shù)字,這兩種標(biāo)識(shí)符用于系統(tǒng)的安全控制。系統(tǒng)通過這兩種標(biāo)識(shí)符控制進(jìn)程對系統(tǒng)中文件和設(shè)備的訪問。

2.3 進(jìn)程之間親屬關(guān)系

系統(tǒng)創(chuàng)建的進(jìn)程具有父/子關(guān)系。因?yàn)橐粋€(gè)進(jìn)程能創(chuàng)建幾個(gè)子進(jìn)程,而子進(jìn)程之間有兄弟關(guān)系。一個(gè)進(jìn)程可能有兩個(gè)父親,一個(gè)為親生父親,一個(gè)為養(yǎng)父。因?yàn)楦高M(jìn)程有可能在子進(jìn)程之前銷毀,就得給子進(jìn)程重新找個(gè)養(yǎng)父,但大多數(shù)情況下,生父和養(yǎng)父是相同的。進(jìn)程間親屬關(guān)系如下圖所示:

Linux進(jìn)程關(guān)系圖

3 進(jìn)程創(chuàng)建與調(diào)度

3.1 進(jìn)程創(chuàng)建

Linux首先通過fork()通過拷貝當(dāng)前進(jìn)程創(chuàng)建一個(gè)子進(jìn)程。然后,exec()函數(shù)負(fù)責(zé)讀取可執(zhí)行文件并將其載入進(jìn)程的地址空間開始運(yùn)行。
  新進(jìn)程是通過克隆父進(jìn)程(當(dāng)前進(jìn)程)而建立的。fork() 和 clone()(用于線程)系統(tǒng)調(diào)用可用來建立新的進(jìn)程。當(dāng)這兩個(gè)系統(tǒng)調(diào)用結(jié)束時(shí),內(nèi)核在內(nèi)存中為新的進(jìn)程分配新的PCB,同時(shí)為新進(jìn)程要使用的堆棧分配物理頁。Linux 還會(huì)為新進(jìn)程分配新的進(jìn)程標(biāo)識(shí)符。然后,新的PCB地址保存在鏈表中,而父進(jìn)程的PCB內(nèi)容被復(fù)制到新進(jìn)程的 PCB中。
  在克隆進(jìn)程時(shí),Linux 允許父子進(jìn)程共享相同的資源??晒蚕淼馁Y源包括文件、信號(hào)處理程序和進(jìn)程地址空間等。當(dāng)某個(gè)資源被共享時(shí),該資源的引用計(jì)數(shù)值會(huì)增加 1,從而只有在兩個(gè)進(jìn)程均終止時(shí),內(nèi)核才會(huì)釋放這些資源。

3.2 進(jìn)程調(diào)度

CFS(完全公平調(diào)度器)是Linux內(nèi)核2.6.23版本開始采用的進(jìn)程調(diào)度器,它的基本原理是這樣的:設(shè)定一個(gè)調(diào)度周期(sched_latency_ns),目標(biāo)是讓每個(gè)進(jìn)程在這個(gè)周期內(nèi)至少有機(jī)會(huì)運(yùn)行一次,換一種說法就是每個(gè)進(jìn)程等待CPU的時(shí)間最長不超過這個(gè)調(diào)度周期;然后根據(jù)進(jìn)程的數(shù)量,大家平分這個(gè)調(diào)度周期內(nèi)的CPU使用權(quán),由于進(jìn)程的優(yōu)先級(jí)即nice值不同,分割調(diào)度周期的時(shí)候要加權(quán);每個(gè)進(jìn)程的累計(jì)運(yùn)行時(shí)間保存在自己的vruntime字段里,哪個(gè)進(jìn)程的vruntime最小就獲得本輪運(yùn)行的權(quán)利。
  新進(jìn)程的vruntime初值的設(shè)置與兩個(gè)參數(shù)有關(guān):

  • sched_child_runs_first:規(guī)定fork之后讓子進(jìn)程先于父進(jìn)程運(yùn)行;
  • sched_features的START_DEBIT位:規(guī)定新進(jìn)程的第一次運(yùn)行要有延遲。
      具體實(shí)現(xiàn)時(shí),CFS通過每個(gè)進(jìn)程的虛擬運(yùn)行時(shí)間(vruntime)來衡量哪個(gè)進(jìn)程最值得被調(diào)度。CFS中的就緒隊(duì)列是一棵以vruntime為鍵值的紅黑樹,虛擬時(shí)間越小的進(jìn)程越靠近整個(gè)紅黑樹的最左端。因此,調(diào)度器每次選擇位于紅黑樹最左端的那個(gè)進(jìn)程,該進(jìn)程的vruntime最小。
      每個(gè)時(shí)鐘周期內(nèi)一個(gè)進(jìn)程的虛擬運(yùn)行時(shí)間是通過下面的方法計(jì)算的:
一次調(diào)度間隔的虛擬運(yùn)行時(shí)間=實(shí)際運(yùn)行時(shí)間*(NICE_0_WEIGHT/權(quán)重)

其中,NICE_0_WEIGHT是nice為0時(shí)的權(quán)重。也就是說,nice值為0的進(jìn)程實(shí)際運(yùn)行時(shí)間和虛擬運(yùn)行時(shí)間相同。通過這個(gè)公式可以看到,權(quán)重越大的進(jìn)程獲得的虛擬運(yùn)行時(shí)間越小,那么它將被調(diào)度器所調(diào)度的機(jī)會(huì)就越大。

4. 進(jìn)程間通信

每個(gè)進(jìn)程各自有不同的用戶地址空間,任何一個(gè)進(jìn)程的全局變量在另一個(gè)進(jìn)程中都看不到,所以進(jìn)程之間要交換數(shù)據(jù)必須通過內(nèi)核,在內(nèi)核中開辟一塊緩沖區(qū),進(jìn)程1把數(shù)據(jù)從用戶空間拷到內(nèi)核緩沖區(qū),進(jìn)程2再從內(nèi)核緩沖區(qū)把數(shù)據(jù)讀走,內(nèi)核提供的這種機(jī)制稱為進(jìn)程間通信,如下圖所示:

進(jìn)程間通信

4.1 管道

4.1.1 匿名管道pipe

  • 匿名管道是半雙工的,數(shù)據(jù)只能向一個(gè)方向流動(dòng);需要雙方通信時(shí),需要建立起兩個(gè)管道
  • 只能用于父子進(jìn)程或者兄弟進(jìn)程之間(具有親緣關(guān)系的進(jìn)程);
  • 單獨(dú)構(gòu)成一種獨(dú)立的文件系統(tǒng):管道對于管道兩端的進(jìn)程而言,就是一個(gè)文件,但它不是普通的文件,它不屬于某種文件系統(tǒng),而是自立門戶,單獨(dú)構(gòu)成一種文件系統(tǒng),并且只存在與內(nèi)存中。
  • 數(shù)據(jù)的讀出和寫入:一個(gè)進(jìn)程向管道中寫的內(nèi)容被管道另一端的進(jìn)程讀出。寫入的內(nèi)容每次都添加在管道緩沖區(qū)的末尾,并且每次都是從緩沖區(qū)的頭部讀出數(shù)據(jù)。如果管道中沒有數(shù)據(jù),讀操縱將被阻塞;如果管道buffer寫滿了,寫操縱將會(huì)被阻塞。

管道是基于文件描述符的通信方式。當(dāng)一個(gè)管道建立時(shí),它會(huì)創(chuàng)建兩個(gè)文件描述符fd[0]和fd[1]。其中fd[0]固定用于讀管道,而fd[1]固定用于寫管道,一般文件I/O的函數(shù)都可以用來操作管道。下面是一個(gè)父子進(jìn)程通過匿名管道通信的代碼示例:

#include <unistd.h>
#include <sys/types.h>
main()
{
    int pipe_fd[2];
    pid_t pid;
    char r_buf[4];
    char** w_buf[256];
    int childexit=0;
    int i;
    int cmd;
    
    memset(r_buf,0,sizeof(r_buf));
    if(pipe(pipe_fd)<0)
    {
        printf("pipe create error\n");
        return -1;
    }
    if((pid=fork())==0)
    //子進(jìn)程:解析從管道中獲取的命令,并作相應(yīng)的處理
    {
        printf("\n");
        close(pipe_fd[1]);
        sleep(2);
        
        while(!childexit)
        {   
            read(pipe_fd[0],r_buf,4);
            cmd=atoi(r_buf);
            if(cmd==0)
            {
printf("child: receive command from parent over\n now child process exit\n");
                childexit=1;
            }
            
               else if(handle_cmd(cmd)!=0)
                return;
            sleep(1);
        }
        close(pipe_fd[0]);
        exit();
    }
    else if(pid>0)
    //parent: send commands to child
    {
    close(pipe_fd[0]);
    w_buf[0]="003";
    w_buf[1]="005";
    w_buf[2]="777";
    w_buf[3]="000";
    for(i=0;i<4;i++)
        write(pipe_fd[1],w_buf[i],4);
    close(pipe_fd[1]);
    }   
}
//下面是子進(jìn)程的命令處理函數(shù)(特定于應(yīng)用):
int handle_cmd(int cmd)
{
if((cmd<0)||(cmd>256))
//suppose child only support 256 commands
    {
    printf("child: invalid command \n");
    return -1;
    }
printf("child: the cmd from parent is %d\n", cmd);
return 0;
}

4.1.2 有名管道fifo

無名管道,由于沒有名字,只能用于親緣關(guān)系的進(jìn)程間通信.。為了克服這個(gè)缺點(diǎn),提出了有名管道(FIFO)。FIFO不同于無名管道之處在于它提供了一個(gè)路徑名與之關(guān)聯(lián),以FIFO的文件形式存在于文件系統(tǒng)中,這樣,即使與FIFO的創(chuàng)建進(jìn)程不存在親緣關(guān)系的進(jìn)程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信,因此,通過FIFO不相關(guān)的進(jìn)程也能交換數(shù)據(jù)。有名管道的名字存在于文件系統(tǒng)中,內(nèi)容存放在內(nèi)存中。有名管道創(chuàng)建的API是:

有名管道創(chuàng)建

下面創(chuàng)建一個(gè)FIFO,并且寫入數(shù)據(jù):

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO_SERVER "/tmp/fifoserver"
main(int argc,char** argv)
//參數(shù)為即將寫入的字節(jié)數(shù)
{
    int fd;
    char w_buf[4096*2];
    int real_wnum;
    memset(w_buf,0,4096*2);
    if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
        printf("cannot create fifoserver\n");
    if(fd==-1)
        if(errno==ENXIO)
            printf("open error; no reading process\n");
        
        fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
    //設(shè)置非阻塞標(biāo)志
    //fd=open(FIFO_SERVER,O_WRONLY,0);
    //設(shè)置阻塞標(biāo)志
    real_wnum=write(fd,w_buf,2048);
    if(real_wnum==-1)
    {
        if(errno==EAGAIN)
            printf("write to fifo error; try later\n");
    }
    else 
        printf("real write num is %d\n",real_wnum);
    real_wnum=write(fd,w_buf,5000);
    //5000用于測試寫入字節(jié)大于4096時(shí)的非原子性
    //real_wnum=write(fd,w_buf,4096);
    //4096用于測試寫入字節(jié)不大于4096時(shí)的原子性
    
    if(real_wnum==-1)
        if(errno==EAGAIN)
            printf("try later\n");
}

下面讀取這個(gè)FIFO寫入的數(shù)據(jù):

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO_SERVER "/tmp/fifoserver"
main(int argc,char** argv)
{
    char r_buf[4096*2];
    int  fd;
    int  r_size;
    int  ret_size;
    r_size=atoi(argv[1]);
    printf("requred real read bytes %d\n",r_size);
    memset(r_buf,0,sizeof(r_buf));
    fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0);
    //fd=open(FIFO_SERVER,O_RDONLY,0);
    //在此處可以把讀程序編譯成兩個(gè)不同版本:阻塞版本及非阻塞版本
    if(fd==-1)
    {
        printf("open %s for read error\n");
        exit(); 
    }
    while(1)
    {
        
        memset(r_buf,0,sizeof(r_buf));
        ret_size=read(fd,r_buf,r_size);
        if(ret_size==-1)
            if(errno==EAGAIN)
                printf("no data avlaible\n");
        printf("real read bytes %d\n",ret_size);
        sleep(1);
    }   
    pause();
    unlink(FIFO_SERVER);
}

4.2 信號(hào)

  • 信號(hào)是進(jìn)程間通信機(jī)制中唯一的異步通信機(jī)制,可以看作是異步通知,通知接收信號(hào)的進(jìn)程有哪些事情發(fā)生了。信號(hào)機(jī)制經(jīng)過POSIX實(shí)時(shí)擴(kuò)展后,功能更加強(qiáng)大,除了基本通知功能外,還可以傳遞附加信息。
  • 如果該進(jìn)程當(dāng)前并未處于執(zhí)行狀態(tài),則該信號(hào)就有內(nèi)核保存起來,知道該進(jìn)程回復(fù)執(zhí)行并傳遞給它為止。
  • 如果一個(gè)信號(hào)被進(jìn)程設(shè)置為阻塞,則該信號(hào)的傳遞被延遲,直到其阻塞被取消是才被傳遞給進(jìn)程。

信號(hào)產(chǎn)生有兩個(gè)來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);軟件來源,最常用發(fā)送信號(hào)的系統(tǒng)函數(shù)是kill, raise, alarm和setitimer以及sigqueue函數(shù),軟件來源還包括一些非法運(yùn)算等操作。

Linux系統(tǒng)中常用信號(hào):
(1)SIGHUP:用戶從終端注銷,所有已啟動(dòng)進(jìn)程都將收到該進(jìn)程。系統(tǒng)缺省狀態(tài)下對該信號(hào)的處理是終止進(jìn)程。
(2)SIGINT:程序終止信號(hào)。程序運(yùn)行過程中,按Ctrl+C鍵將產(chǎn)生該信號(hào)。
(3)SIGQUIT:程序退出信號(hào)。程序運(yùn)行過程中,按Ctrl+\鍵將產(chǎn)生該信號(hào)。
(4)SIGBUS和SIGSEGV:進(jìn)程訪問非法地址。
(5)SIGFPE:運(yùn)算中出現(xiàn)致命錯(cuò)誤,如除零操作、數(shù)據(jù)溢出等。
(6)SIGKILL:用戶終止進(jìn)程執(zhí)行信號(hào)。shell下執(zhí)行kill -9發(fā)送該信號(hào)。
(7)SIGTERM:結(jié)束進(jìn)程信號(hào)。shell下執(zhí)行kill 進(jìn)程pid發(fā)送該信號(hào)。
(8)SIGALRM:定時(shí)器信號(hào)。
(9)SIGCLD:子進(jìn)程退出信號(hào)。如果其父進(jìn)程沒有忽略該信號(hào)也沒有處理該信號(hào),則子進(jìn)程退出后將形成僵尸進(jìn)程。

進(jìn)程可以通過三種方式來響應(yīng)一個(gè)信號(hào):(1)忽略信號(hào),即對信號(hào)不做任何處理,其中,有兩個(gè)信號(hào)不能忽略:SIGKILL及SIGSTOP;(2)捕捉信號(hào)。定義信號(hào)處理函數(shù),當(dāng)信號(hào)發(fā)生時(shí),執(zhí)行相應(yīng)的處理函數(shù);(3)執(zhí)行缺省操作,Linux對每種信號(hào)都規(guī)定了默認(rèn)操作。

下面是一個(gè)alarm信號(hào)使用例子:

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

static int alarm_fired = 0;

void ouch(int sig)
{
    alarm_fired = 1;
}

int main()
{
    //關(guān)聯(lián)信號(hào)處理函數(shù)
    signal(SIGALRM, ouch);
    //調(diào)用alarm函數(shù),5秒后發(fā)送信號(hào)SIGALRM
    alarm(5);
    //掛起進(jìn)程
    pause();
    //接收到信號(hào)后,恢復(fù)正常執(zhí)行
    if(alarm_fired == 1)
        printf("Receive a signal %d\n", SIGALRM);
    exit(0);
}

注:如果父進(jìn)程在子進(jìn)程的信號(hào)到來之前沒有事情可做,我們可以用函數(shù)pause()來掛起父進(jìn)程,直到父進(jìn)程接收到信號(hào)。當(dāng)進(jìn)程接收到一個(gè)信號(hào)時(shí),預(yù)設(shè)好的信號(hào)處理函數(shù)將開始運(yùn)行,程序也將恢復(fù)正常的執(zhí)行。這樣可以節(jié)省CPU的資源,因?yàn)榭梢员苊馐褂靡粋€(gè)循環(huán)來等待。

4.3 消息隊(duì)列

消息隊(duì)列是消息的鏈表,存放在內(nèi)核中并由消息隊(duì)列標(biāo)識(shí)符標(biāo)識(shí)。在某個(gè)進(jìn)程往一個(gè)隊(duì)列寫入消息之前,并不需要另外某個(gè)進(jìn)程在該隊(duì)列上等待消息的到達(dá)。這跟管道和FIFO是相反的,對后兩者來說,除非讀出者已存在,否則先有寫入者是沒有意義的。
  管道和FIFO都是隨進(jìn)程持續(xù)的,XSI IPC(消息隊(duì)列、信號(hào)量、共享內(nèi)存)都是隨內(nèi)核持續(xù)的。當(dāng)一個(gè)管道或FIFO的最后一次關(guān)閉發(fā)生時(shí),仍在該管道或FIFO上的數(shù)據(jù)將被丟棄。消息隊(duì)列,除非系統(tǒng)重啟或顯式刪除,否則其一直存在。
 對于系統(tǒng)中的每個(gè)消息隊(duì)列,內(nèi)核維護(hù)一個(gè)定義在<sys/msg.h>頭文件中的信息結(jié)構(gòu)。

struct msqid_ds {
    struct ipc_perm msg_perm ; 
    struct msg*    msg_first ; //指向隊(duì)列中的第一個(gè)消息
    struct msg*    msg_last ; //指向隊(duì)列中的最后一個(gè)消息
    ……
} ;

下面是一個(gè)接收消息隊(duì)列數(shù)據(jù)的代碼:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>

struct msg_st
{
    long int msg_type;
    char text[BUFSIZ];
};

int main()
{
    int running = 1;
    int msgid = -1;
    struct msg_st data;
    long int msgtype = 0; //注意1

    //建立消息隊(duì)列
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
    if(msgid == -1)
    {
        fprintf(stderr, "msgget failed with error: %d\n", errno);
        exit(EXIT_FAILURE);
    }
    //從隊(duì)列中獲取消息,直到遇到end消息為止
    while(running)
    {
        if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
        {
            fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
            exit(EXIT_FAILURE);
        }
        printf("You wrote: %s\n",data.text);
        //遇到end結(jié)束
        if(strncmp(data.text, "end", 3) == 0)
            running = 0;
    }
    //刪除消息隊(duì)列
    if(msgctl(msgid, IPC_RMID, 0) == -1)
    {
        fprintf(stderr, "msgctl(IPC_RMID) failed\n");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}

下面是對消息隊(duì)列寫消息的例子:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>

#define MAX_TEXT 512
struct msg_st
{
    long int msg_type;
    char text[MAX_TEXT];
};

int main()
{
    int running = 1;
    struct msg_st data;
    char buffer[BUFSIZ];
    int msgid = -1;

    //建立消息隊(duì)列
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
    if(msgid == -1)
    {
        fprintf(stderr, "msgget failed with error: %d\n", errno);
        exit(EXIT_FAILURE);
    }

    //向消息隊(duì)列中寫消息,直到寫入end
    while(running)
    {
        //輸入數(shù)據(jù)
        printf("Enter some text: ");
        fgets(buffer, BUFSIZ, stdin);
        data.msg_type = 1;    //注意2
        strcpy(data.text, buffer);
        //向隊(duì)列發(fā)送數(shù)據(jù)
        if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
        {
            fprintf(stderr, "msgsnd failed\n");
            exit(EXIT_FAILURE);
        }
        //輸入end結(jié)束輸入
        if(strncmp(buffer, "end", 3) == 0)
            running = 0;
        sleep(1);
    }
    exit(EXIT_SUCCESS);
}

4.4 共享內(nèi)存

采用共享內(nèi)存通信的一個(gè)顯而易見的好處是效率高,因?yàn)檫M(jìn)程可以直接讀寫內(nèi)存,而不需要任何數(shù)據(jù)的拷貝。對于像管道和消息隊(duì)列等通信方式,則需要在內(nèi)核和用戶空間進(jìn)行四次的數(shù)據(jù)拷貝,而共享內(nèi)存則只拷貝兩次數(shù)據(jù):

  • 一次從輸入文件到共享內(nèi)存區(qū)
  • 另一次從共享內(nèi)存區(qū)到輸出文件。

現(xiàn)代Linux有兩種共享內(nèi)存機(jī)制:

  • POSIX共享內(nèi)存(shm_open()、shm_unlink())
  • System V共享內(nèi)存(shmget()、shmat()、shmdt())

其中,System V共享內(nèi)存歷史悠久;而POSIX共享內(nèi)存機(jī)制接口更加方便易用,一般是結(jié)合內(nèi)存映射mmap使用。

mmap和System V共享內(nèi)存的主要區(qū)別在于:

  • sysv shm是持久化的,除非被一個(gè)進(jìn)程明確的刪除,否則它始終存在于內(nèi)存里,直到系統(tǒng)關(guān)機(jī);
  • mmap映射的內(nèi)存在不是持久化的,如果進(jìn)程關(guān)閉,映射隨即失效,除非事先已經(jīng)映射到了一個(gè)文件上。

內(nèi)存映射機(jī)制mmap是POSIX標(biāo)準(zhǔn)的系統(tǒng)調(diào)用,有匿名映射和文件映射兩種。

  • 匿名映射使用進(jìn)程的虛擬內(nèi)存空間,它和malloc(3)類似,實(shí)際上有些malloc實(shí)現(xiàn)會(huì)使用mmap匿名映射分配內(nèi)存,不過匿名映射不是POSIX標(biāo)準(zhǔn)中規(guī)定的。
  • 文件映射有MAP_PRIVATE和MAP_SHARED兩種。前者使用COW的方式,把文件映射到當(dāng)前的進(jìn)程空間,修改操作不會(huì)改動(dòng)源文件。后者直接把文件映射到當(dāng)前的進(jìn)程空間,所有的修改會(huì)直接反應(yīng)到文件的page cache,然后由內(nèi)核自動(dòng)同步到映射文件上。

相比于IO函數(shù)調(diào)用,基于文件的mmap的一大優(yōu)點(diǎn)是把文件映射到進(jìn)程的地址空間,避免了數(shù)據(jù)從用戶緩沖區(qū)到內(nèi)核page cache緩沖區(qū)的復(fù)制過程;當(dāng)然還有一個(gè)優(yōu)點(diǎn)就是不需要頻繁的read/write系統(tǒng)調(diào)用, 比較適合隨機(jī)讀取文件內(nèi)容的場景。

由于接口易用,且可以方便的persist到文件,避免主機(jī)shutdown丟失數(shù)據(jù)的情況,所以在現(xiàn)代操作系統(tǒng)上一般偏向于使用mmap而不是傳統(tǒng)的System V的共享內(nèi)存機(jī)制。

建議僅把mmap用于需要大量內(nèi)存數(shù)據(jù)操作的場景,而不用于IPC。因?yàn)镮PC總是在多個(gè)進(jìn)程之間通信,而通信則涉及到同步問題,如果自己手工在mmap之上實(shí)現(xiàn)同步,容易滋生bug。推薦使用socket之類的機(jī)制做IPC,基于socket的通信機(jī)制相對健全很多,有很多成熟的機(jī)制和模式,比如epoll, reactor等。

下面是一個(gè)mmap共享內(nèi)存的范例:

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


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


int main()
{
    int fd = open("/home/jay/linux/test.txt",O_CREAT|O_WRONLY,0666); //1
    if(-1 == fd)
        ERR_EXIT("open");

    struct stat buf;
    fstat(fd,&buf);
    void* p = mmap(NULL,(int)buf.st_size,PROT_WRITE,MAP_SHARED,fd,0);

    if(MAP_FAILED == p)   //MAP_FAILED 是一個(gè)宏 等于 (void*)-1
        ERR_EXIT("mmap");

    strcpy(p,"hello world");

    if(-1 == munmap(p,buf.st_size))
        ERR_EXIT("munmp");

    return 0;
}

下面是一個(gè)system V api實(shí)現(xiàn)的內(nèi)存共享范例:

#include<stdio.h>
#include<sys/types.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/shm.h>

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


int main()
{
    key_t key=ftok("/tmp",12343);
    if(-1 == key)
        ERR_EXIT("ftok");

    int shmid = shmget(key,1024,IPC_CREAT|IPC_EXCL|0666);  //創(chuàng)建一個(gè)共享內(nèi)存
    if(-1 == shmid)
        ERR_EXIT("shmget");

    void* p = shmat(shmid,NULL,0);  
    if((void*)-1 == p)
        ERR_EXIT("shmat");

    strcpy(p,"11111111111");

    shmdt(p);    //只是斷開連接而已

    return 0;
}

4.5 信號(hào)量

為了防止出現(xiàn)因多個(gè)程序同時(shí)訪問一個(gè)共享資源而引發(fā)的一系列問題,我們需要一種方法,它可以通過生成并使用令牌來授權(quán),在任一時(shí)刻只能有一個(gè)執(zhí)行線程訪問代碼的臨界區(qū)域。臨界區(qū)域是指執(zhí)行數(shù)據(jù)更新的代碼需要獨(dú)占式地執(zhí)行。而信號(hào)量就可以提供這樣的一種訪問機(jī)制,讓一個(gè)臨界區(qū)同一時(shí)間只有一個(gè)線程在訪問它,也就是說信號(hào)量是用來調(diào)協(xié)進(jìn)程對共享資源的訪問的。
  信號(hào)量是一個(gè)特殊的變量,程序?qū)ζ湓L問都是原子操作,且只允許對它進(jìn)行等待(即P(信號(hào)變量))和發(fā)送(即V(信號(hào)變量))信息操作。最簡單的信號(hào)量是只能取0和1的變量,這也是信號(hào)量最常見的一種形式,叫做二進(jìn)制信號(hào)量。而可以取多個(gè)正整數(shù)的信號(hào)量被稱為通用信號(hào)量。這里主要討論二進(jìn)制信號(hào)量。
  由于信號(hào)量只能進(jìn)行兩種操作等待和發(fā)送信號(hào),即P(sv)和V(sv),他們的行為是這樣的:

  • P(sv):如果sv的值大于零,就給它減1;如果它的值為零,就掛起該進(jìn)程的執(zhí)行
  • V(sv):如果有其他進(jìn)程因等待sv而被掛起,就讓它恢復(fù)運(yùn)行,如果沒有進(jìn)程因等待sv而掛起,就給它加1.

下面是一個(gè)信號(hào)量實(shí)現(xiàn)臨界區(qū)訪問的示例代碼:

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

union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
};

static int sem_id = 0;

static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();

int main(int argc, char *argv[])
{
    char message = 'X';
    int i = 0;

    //創(chuàng)建信號(hào)量
    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

    if(argc > 1)
    {
        //程序第一次被調(diào)用,初始化信號(hào)量
        if(!set_semvalue())
        {
            fprintf(stderr, "Failed to initialize semaphore\n");
            exit(EXIT_FAILURE);
        }
        //設(shè)置要輸出到屏幕中的信息,即其參數(shù)的第一個(gè)字符
        message = argv[1][0];
        sleep(2);
    }
    for(i = 0; i < 10; ++i)
    {
        //進(jìn)入臨界區(qū)
        if(!semaphore_p())
            exit(EXIT_FAILURE);
        //向屏幕中輸出數(shù)據(jù)
        printf("%c", message);
        //清理緩沖區(qū),然后休眠隨機(jī)時(shí)間
        fflush(stdout);
        sleep(rand() % 3);
        //離開臨界區(qū)前再一次向屏幕輸出數(shù)據(jù)
        printf("%c", message);
        fflush(stdout);
        //離開臨界區(qū),休眠隨機(jī)時(shí)間后繼續(xù)循環(huán)
        if(!semaphore_v())
            exit(EXIT_FAILURE);
        sleep(rand() % 2);
    }

    sleep(10);
    printf("\n%d - finished\n", getpid());

    if(argc > 1)
    {
        //如果程序是第一次被調(diào)用,則在退出前刪除信號(hào)量
        sleep(3);
        del_semvalue();
    }
    exit(EXIT_SUCCESS);
}

static int set_semvalue()
{
    //用于初始化信號(hào)量,在使用信號(hào)量前必須這樣做
    union semun sem_union;

    sem_union.val = 1;
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
        return 0;
    return 1;
}

static void del_semvalue()
{
    //刪除信號(hào)量
    union semun sem_union;

    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        fprintf(stderr, "Failed to delete semaphore\n");
}

static int semaphore_p()
{
    //對信號(hào)量做減1操作,即等待P(sv)
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = -1;//P()
    sem_b.sem_flg = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, "semaphore_p failed\n");
        return 0;
    }
    return 1;
}

static int semaphore_v()
{
    //這是一個(gè)釋放操作,它使信號(hào)量變?yōu)榭捎茫窗l(fā)送信號(hào)V(sv)
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = 1;//V()
    sem_b.sem_flg = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, "semaphore_v failed\n");
        return 0;
    }
    return 1;
}

4.6 套接字

套接字(socket)是一種通信機(jī)制,憑借這種機(jī)制,客戶/服務(wù)器(即要進(jìn)行通信的進(jìn)程)系統(tǒng)的開發(fā)工作既可以在本地單機(jī)上進(jìn)行,也可以跨網(wǎng)絡(luò)進(jìn)行。也就是說它可以讓不在同一臺(tái)計(jì)算機(jī)但通過網(wǎng)絡(luò)連接計(jì)算機(jī)上的進(jìn)程進(jìn)行通信。

socket通信流程

5 進(jìn)程相關(guān)命令

5.1 ps/pstree

ps命令參考前文 Linux內(nèi)存 這里還有額外補(bǔ)充一下關(guān)系進(jìn)程狀態(tài)的說明。通過ps aux可以看到進(jìn)程的狀態(tài)。

O:進(jìn)程正在處理器運(yùn)行,這個(gè)狀態(tài)從來沒有見過.
S:休眠狀態(tài)(sleeping)
R:等待運(yùn)行(runable)R Running or runnable (on run queue) 進(jìn)程處于運(yùn)行或就緒狀態(tài)
I:空閑狀態(tài)(idle)
Z:僵尸狀態(tài)(zombie)
T:跟蹤狀態(tài)(Traced)
B:進(jìn)程正在等待更多的內(nèi)存頁
D: 不可中斷的深度睡眠,一般由IO引起,同步IO在做讀或?qū)懖僮鲿r(shí),cpu不能做其它事情,只能等待,這時(shí)進(jìn)程處于這種狀態(tài),如果程序采用異步IO,這種狀態(tài)應(yīng)該就很少見到了。

pstree命令將所有進(jìn)程以樹狀圖顯示,基本用法如下描述:

pstree [-a] [-c] [-h|-Hpid] [-l] [-n] [-p] [-u] [-G|-U] [pid|user]

下面是一個(gè)pstree命令的實(shí)例:

pstree
init─┬─acpid
     ├─agetty
     ├─bcron_start───sleep
     ├─bns_nginx───bns_nginx
     ├─casio-loader64───scribed───5*[{scribed}]
     ├─casio-loader64───casio-agent───5*[{casio-agent}]
     ├─casio-loader64───log_counter_col───3*[{log_counter_col}]

5.2 kill/killall

kill 的用法:

kill [信號(hào)代碼] 進(jìn)程ID(kill  -pid)
-s:指定發(fā)送的信號(hào)。 
-p:模擬發(fā)送信號(hào)。 
-l:指定信號(hào)的名稱列表。 
pid:要中止進(jìn)程的ID號(hào)。 
Signal:表示信號(hào)。
注:信號(hào)代碼可以省略, 默認(rèn)發(fā)送的信號(hào)是 15) SIGTERM;我們常用的信號(hào)代碼是-9 ,表示強(qiáng)制終止;對于僵尸進(jìn)程,可以用kill -9 來強(qiáng)制終止退出;

killall命令使用進(jìn)程的名稱來殺死進(jìn)程,使用此指令可以殺死一組同名進(jìn)程。我們可以使用kill命令殺死指定進(jìn)程PID的進(jìn)程,如果要找到我們需要?dú)⑺赖倪M(jìn)程,我們還需要在之前使用ps等命令再配合grep來查找進(jìn)程,而killall把這兩個(gè)過程合二為一,是一個(gè)很好用的命令, 用法:

killall 參數(shù) 進(jìn)程名字
-e:對長名稱進(jìn)行精確匹配;
-l:忽略大小寫的不同; 
-p:殺死進(jìn)程所屬的進(jìn)程組;
-i:交互式殺死進(jìn)程,殺死進(jìn)程前需要進(jìn)行確認(rèn); 
-l:打印所有已知信號(hào)列表;
-q:如果沒有進(jìn)程被殺死。則不輸出任何信息;
-r:使用正規(guī)表達(dá)式匹配要?dú)⑺赖倪M(jìn)程名稱; 
-s:用指定的進(jìn)程號(hào)代替默認(rèn)信號(hào)“SIGTERM”;
-u:殺死指定用戶的進(jìn)程。

5.3 ipcs/ipcrm

ipcs命令用于報(bào)告Linux中進(jìn)程間通信設(shè)施的狀態(tài),顯示的信息包括消息列表、共享內(nèi)存和信號(hào)量的信息。用法如下:

ipcs 選項(xiàng)
-a:顯示全部可顯示的信息; 
-q:顯示活動(dòng)的消息隊(duì)列信息; 
-m:顯示活動(dòng)的共享內(nèi)存信息; 
-s:顯示活動(dòng)的信號(hào)量信息。

下面是一個(gè)ipcs命令輸出

ipcs -a

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

ipcrm命令用來刪除一個(gè)或更多的消息隊(duì)列、信號(hào)量集或者共享內(nèi)存標(biāo)識(shí)?;居梅ㄈ缦旅枋觯?/p>

 ipcrm [-M shmkey] [-m shmid] [-Q msgkey] [-q msqid] [-S semkey] [-s semid] ...

-m SharedMemory id 刪除共享內(nèi)存標(biāo)識(shí) SharedMemoryID。與 SharedMemoryID 有關(guān)聯(lián)的共享內(nèi)存段以及數(shù)據(jù)結(jié)構(gòu)都會(huì)在最后一次拆離操作后刪除。 
-M SharedMemoryKey 刪除用關(guān)鍵字 SharedMemoryKey 創(chuàng)建的共享內(nèi)存標(biāo)識(shí)。與其相關(guān)的共享內(nèi)存段和數(shù)據(jù)結(jié)構(gòu)段都將在最后一次拆離操作后刪除。 
-q MessageID 刪除消息隊(duì)列標(biāo)識(shí) MessageID 和與其相關(guān)的消息隊(duì)列和數(shù)據(jù)結(jié)構(gòu)。 
-Q MessageKey 刪除由關(guān)鍵字 MessageKey 創(chuàng)建的消息隊(duì)列標(biāo)識(shí)和與其相關(guān)的消息隊(duì)列和數(shù)據(jù)結(jié)構(gòu)。 
-s SemaphoreID 刪除信號(hào)量標(biāo)識(shí) SemaphoreID 和與其相關(guān)的信號(hào)量集及數(shù)據(jù)結(jié)構(gòu)。 
-S SemaphoreKey 刪除由關(guān)鍵字 SemaphoreKey 創(chuàng)建的信號(hào)標(biāo)識(shí)和與其相關(guān)的信號(hào)量集和數(shù)據(jù)結(jié)構(gòu)。

6. 其他

6.1 進(jìn)程/線程context switch消耗

線程和進(jìn)程在內(nèi)核的管理結(jié)構(gòu) task struct是一樣的,切換的差別也只是是否刷新TLB,刷新TLB本身不費(fèi)時(shí),只是刷新后,TLB miss會(huì)比較影響性能,但這不能算在“切換”時(shí)間內(nèi)。真正的切換時(shí)間只有寄存器的保持到內(nèi)存中,并把將要執(zhí)行的線程/進(jìn)程的上下文從內(nèi)存拷貝到寄存器中,就完成了一次切換,非???。

7. 參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容